Golang问题点(三) - Context的问题点

Go的标准库 context

在 Go http包的Server中,每一个请求在都有一个对应的 goroutine 去处理。请求处理函数通常会启动额外的 goroutine 用来访问后端服务,比如数据库和RPC服务。用来处理一个请求的 goroutine 通常需要访问一些与请求特定的数据,比如终端用户的身份认证信息、验证相关的token、请求的截止时间。 当一个请求被取消或超时时,所有用来处理该请求的 goroutine 都应该迅速退出,然后系统才能释放这些 goroutine 占用的资源。

那么如何控制goroutine优雅的退出呢?

1. 实例

1.0. 基础代码

package main

import (
    "fmt"
    "sync"
    "time"
)

var wg sync.WaitGroup

func DoSomething() {
    for {
        fmt.Println("董小贱")
        time.Sleep(2 * time.Second)
    }
    wg.Done()
}

func main() {
    wg.Add(1)
  go DoSomething()  //   这个DoSomething() 没有停止条件,不会停止
    wg.Wait()
    fmt.Println("DoSomething over")
}

1. 1全局变量方式

package main

import (
    "fmt"
    "sync"
    "time"
)

var wg sync.WaitGroup
var status bool = true

func DoSomething() {
    for {
        fmt.Println("董小贱")
        time.Sleep(2 * time.Second)
        if !status {
            break
        } else {
            continue
        }
    }
    wg.Done()
}

func main() {
    wg.Add(1)
    go DoSomething()
    time.Sleep(10 * time.Second)
    status = false  //通过修改全局变量的状态,给gorotine发送终止信号
    wg.Wait()
    fmt.Println("DoSomething over")
}

1.2. select + channel 版本

package main

import (
    "fmt"
    "sync"
    "time"
)

var wg sync.WaitGroup
var chann = make(chan bool)

func DoSomething() {
LoopTag:
    for {
        fmt.Println("董小贱")
        time.Sleep(2 * time.Second)
        select {
        case <-chann: // 信号接收
            break LoopTag // 退出整个lable
        default:
            continue
        }
    }
    wg.Done()
}

func main() {
    wg.Add(1)
    go DoSomething()
    time.Sleep(10 * time.Second)
    chann <- false // 通过无缓存的channel来给goroutine发送终止信号
    wg.Wait()
    fmt.Println("DoSomething over")
}

1.3.context版本

package main

import (
    "context"
    "fmt"
    "sync"
    "time"
)

var wg sync.WaitGroup

func DoSomething(ctx context.Context) {
LoopTag:
    for {
        fmt.Println("董小贱")
        time.Sleep(2 * time.Second)
        select {
        case <-ctx.Done(): // 等待父级通知
            break LoopTag
        default:
            continue
        }
    }
    wg.Done()

}

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    wg.Add(1)
    go DoSomething(ctx)
    time.Sleep(10 * time.Second)
    cancel() //cancel()执行,通知子goroutine结束
    wg.Wait()
    fmt.Println("DoSomething over")
}

当在DoSomething中再开启子goroutine时,将ctx传入到函数当中即可:

package main

import (
    "context"
    "fmt"
    "sync"
    "time"
)

var wg sync.WaitGroup

func DoSomething2(ctx context.Context) {
LoopTag:
    for {
        fmt.Println("董小小贱")
        time.Sleep(2 * time.Second)
        select {
        case <-ctx.Done():
            break LoopTag
        default:
            continue
        }
    }
    wg.Done()
}

func DoSomething(ctx context.Context) {
    go DoSomething2(ctx)
LoopTag:
    for {
        fmt.Println("董小贱")
        time.Sleep(2 * time.Second)
        select {
        case <-ctx.Done():
            break LoopTag
        default:
            continue
        }
    }
    wg.Done()

}

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    wg.Add(1)
    go DoSomething(ctx)
    time.Sleep(10 * time.Second)
    cancel()
    wg.Wait()
    fmt.Println("DoSomething over")
}

那么context的作用就呼之欲出了:简单讲就是用来处理goroutine之间传递截止日期、取消信号和其他请求范围内的值

2. context相关


Go1.7 加入的新的标准库context 它定义了Context类型, 它跨API边界和进程之间传递截止日期、取消信号和其他请求范围内的值。

对服务器的传入请求应该创建上下文,而对服务器的传出调用应该接受上下文。它们之间的函数调用链必须传播上下文,可以选择将其替换为使用WithCancelWithDeadlineWithTimeoutWithValue创建的派生上下文。当一个上下文被取消时,从它派生的所有上下文也被取消。

WithCancelWithDeadlineWithTimeout函数接收一个Context并返回一个派生子Context和一个CancelFunc。调用CancelFunc取消子进程及其子进程,删除父进程对子进程的引用,并停止所有相关的计时器。如果没有调用CancelFunc,则会泄漏子节点及其子节点,直到父节点被取消或计时器触发。

不要将Context存储在结构类型中,应该显示地将上下文传递给需要它的每个函数,Context应该是第一个参数,通常命名为ctx。


2.1 context包相关

2.1.1 context.Context是一个接口,该接口定义了四个需要实现的方法。具体签名如下:
type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{}
    Err() error
    Value(key interface{}) interface{}
}


// Deadline方法需要返回当前Context被取消的时间,也就是完成工作的截止时间(deadline);
//Done方法需要返回一个Channel,这个Channel会在当前工作完成或者上下文被取消之后关闭,多次调用Done方法会返回同一个Channel;
//Err方法会返回当前Context结束的原因,它只会在Done返回的Channel被关闭时才会返回非空的值;
        //如果当前Context被取消就会返回Canceled错误;
        //如果当前Context超时就会返回DeadlineExceeded错误;
//Value方法会从Context中返回键对应的值,对于同一个上下文来说,多次调用Value 并传入相同的Key会返回相同的结果,该方法仅用于传递跨API和进程间跟请求域的数据;
2.1.2 CancelFunc
type CancelFunc func()

// CancelFunc告诉一个goroutine放弃它的工作。CancelFunc不会等待工作停止。在第一次调用之后,对CancelFunc的后续调用什么也不做。
2.1.3 Background()
func Background() Context

// Background返回一个非nil的空上下文。它从未被取消,没有值,也没有期限。它通常由主函数、初始化和测试使用,并作为传入请求的根context(最顶层的context,其他的都是它的派生子context)
2.1.4 TODO()
func TODO() Context

//TODO返回一个非nil的空context。代码应该使用context。TODO当不清楚要使用哪个context或者它还不可用时(因为周围的函数还没有被扩展到接受context参数)。TODO由静态分析工具识别,该工具确定context是否在程序中正确传播

//这个就是知道要用context,但不清楚用哪个,埋点用。
backgroundtodo本质上都是emptyCtx结构体类型,是一个不可取消,没有设置截止时间,没有携带任何值的Context。

2.2 With系列函数

2.2.1 WithCancel
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)

//WithCancel返回带有新Done通道的父进程的一个副本。当返回的cancel函数被调用时,或者当父context的Done通道被关闭时,返回context的Done通道将被关闭,以最先发生的情况为准。

// 取消此context释放与之关联的资源,因此代码应该在此context中运行的操作完成后立即调用cancel。

具体用法可以看上边的例子。

2.2.2 WithDeadline
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)

// deadline 是绝对时间,比如:d := time.Now().Add(50 * time.Millisecond)

//WithDeadline返回父context的一个副本,并将deadline调整为不迟于d。如果父上下文的deadline已经早于d,则WithDeadline(parent, d)在语义上等同于父上下文。返回的上下文的Done通道在deadline过期时关闭,在调用返回的cancel函数时关闭,或者在关闭父上下文的Done通道时关闭,以最先发生的情况为准。

// 取消此上下文释放与之关联的资源,因此代码应该在此上下文中运行的操作完成后立即调用cancel。

一个例子:

func main() {
    d := time.Now().Add(50 * time.Millisecond)
    ctx, cancel := context.WithDeadline(context.Background(), d)
    defer cancel()
    select {
    case <-time.After(1 * time.Second):
        fmt.Println("overslept")
    case <-ctx.Done():
        fmt.Println(ctx.Err())
    }
}


// 这里会先执行ctx.Done() 然后打印异常
2.2.1 WithTimeout
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

// 跟WithDeadline的区别在于,WithTimeout是用的相对时间。
2.2.1 WithValue
func WithValue(parent Context, key, val interface{}) Context

//WithValue返回父节点的一个副本,其中与键关联的值为val。

//只将上下文值用于传输流程和api的请求范围的数据,而不用于将可选参数传递给函数。

// 提供的键必须是可比较的,并且不应该是string类型或任何其他内置类型,以避免使用context的包之间的冲突。WithValue的用户应该定义自己的键类型。为了避免在分配给interface{}时进行分配,上下文键通常具有具体类型struct{}。或者,导出的上下文关键变量的静态类型应该是指针或接口。

一个例子:

package main

import (
    "context"
    "fmt"
)

type favContextKey string

func main() {

    f := func(ctx context.Context, k favContextKey) {
        if v := ctx.Value(k); v != nil {
            fmt.Println("found value:", v)
            return
        }
        fmt.Println("key not found:", k)
    }

    k := favContextKey("language")
    ctx := context.WithValue(context.Background(), k, "Go")

    f(ctx, k)
    f(ctx, favContextKey("color"))
}

2.3 context的注意事项:

- 推荐以参数的方式显示传递Context
- 以Context作为参数的函数方法,应该把Context作为第一个参数,显示传递。
​- 给一个函数方法传递Context的时候,不要传递nil,如果不知道传递什么,就使用context.TODO()
- Context的Value相关方法应该传递请求域的必要数据,不应该用于传递可选参数
- Context是线程安全的,可以放心的在多个goroutine中传递

你可能感兴趣的:(Golang问题点(三) - Context的问题点)