Golang 标准库 tips -- context

908 阅读7分钟

本文目录结构:

gls
假如没有 Context
context 的实现原理
context 主要功能
	实现超时控制
	实现错误取消
	防止 groutine 泄露
	实现数据同步
获取 context 里面所有的元素
context 案例分析 http.Requst
context 案例分析 gin.Context
context 调用栈太深问题
context 的最佳实践

gls

Go 没有 gls(goroutine local storage) 协程本地存储的概念,不能将数据和 groutine 绑定从而在groutine的调用链路上获取绑定的数据,如果需要传递数据到后续的方法中,必须通过参数传递的方式,否则可以每个 goroutine 绑定了一个 map,在 goroutine 的存在时间中, 可以调用 gls 的接口, 向这个 map 中存入或读取数据。有了GLS, 可以直接将数据和当前的goroutine进行绑定。比如当前的 goroutine 将会进行一系列很深的调用, 想控制调用的超时可以这样做: 调用开始时, 利用 setGLS, 放入你的调用起始和超时时间; 在每层调用中, 利用getGLS取出起始和超时时间; 进行检测, 发现如果已经超时, 则直接返回, 不再向下进行调用。 虽然有一些 gls 的库(eg: github.com/qw4990/blog… tricky 功能,但是并不推荐。

假如没有 Context

Go 在 1.7 正式引入了 context 的功能,在了解 context 之前,我们可以来看下 Go 的一些并发场景下如果没有 Context 应该怎样来写,又会有哪些问题。 在 Go http 包的 Server 中,每一个请求在都有一个对应的 groutine 去处理,这些 goroutine 又可能会开启其它的 goroutine,当一个请求被取消或超时时,所有用来处理该请求的 goroutine 都应该迅速退出,然后系统才能释放这些 goroutine 占用的资源,如果没有 context,我们是可以通过 channel 和 select 来实现。 如下例子在主协程中模拟启动一个 groutine,并在 groutine 中模拟启动一个非常耗时的 rpc 接口,然后控制子 groutine 的超时时间是 2s,超过 2s 则往下执行,可以通过 channel 的 close 方法来实现。

func main() {
    ch := make(chan struct{})

    go handle(ch)

    time.Sleep(2 * time.Second) // 2 秒超时
    close(ch)

    time.Sleep(2 * time.Second)
}

// 模拟一个耗时的 rpc 请求
func rpc() (string, error) {
    time.Sleep(3 * time.Second)
    return "success", nil
}

type result struct {
    Data string
    Err  error
}

func handle(ch chan struct{}) {
    r := make(chan result)

    go func() {
        data, err := rpc()
        r <- result{data, err}
    }()

    select {
    case <-ch:
        log.Println("request is canceled")
        return
    case res := <-r:
        log.Printf("rpc success, data: %v, err: %v", res.Data, res.Err)
    }
}

但是如果我们有一种场景是主线程中启动多个 groutine,每个 groutine 的超时时间不一样,并且对这些 groutine 有一个整体的超时时间,这种情况下我们就需要通过多个 channel 来实现,以下是模拟在 main groutine 中启动 2个 groutine,并且每个 groutine 的超时时间已经整个 main groutine 的超时时间都不一样,这种情况下使用了 3 个 channel 来实现,代码比较丑陋,而且也很容易出错。

func main() {
    ch := make(chan struct{})
    ch1 := make(chan struct{})
    ch2 := make(chan struct{})

    go handle1(ch, ch1)
    go handle2(ch, ch2)

    go func() {
        time.Sleep(4 * time.Second) // handle1, handle2 整体超时时间
        close(ch)
    }()

    go func() {
        time.Sleep(2 * time.Second) // handle1 整体超时时间
        close(ch1)
    }()

    go func() {
        time.Sleep(6 * time.Second) // handle2 整体超时时间
        close(ch2)
    }()

    time.Sleep(10 * time.Second)
}

func rpc1() (string, error) {
    time.Sleep(3 * time.Second)
    return "success", nil
}

func rpc2() (string, error) {
    time.Sleep(5 * time.Second)
    return "success", nil
}

type result struct {
    Data string
    Err  error
}

func handle1(ch, ch1 chan struct{}) {
    r := make(chan result)

    go func() {
        data, err := rpc1()
        r <- result{data, err}
    }()

    select {
    case <-ch:
        log.Println("handle1 main groutine timeout")
        return
    case <-ch1:
        log.Println("handle1 timeout")
        return
    case res := <-r:
        log.Printf("rpc success, data: %v, err: %v", res.Data, res.Err)
        return
    }
}

func handle2(ch, ch2 chan struct{}) {
    r := make(chan result)

    go func() {
        data, err := rpc2()
        r <- result{data, err}
    }()

    select {
    case <-ch:
        log.Println("handle2 main groutine timeout")
        return
    case <-ch2:
        log.Println("handle2 timeout")
        return
    case res := <-r:
        log.Printf("rpc success, data: %v, err: %v", res.Data, res.Err)
        return
    }
}

// 代码输出:
handle1 timeout
handle2 main groutine timeout

再看另一种场景,我们需要跟踪整个链路的调用情况,需要把这些调用给串联起来,因为 golang 的 groutine 不像其他语言比如 Java 有 ThreadLocal 属于当前线程的变量,如果没有 context,那我们需要把这些参数传递到每一个方法中,如果需要记录更多的信息,要么就是加更多的参数,要么要自己来封装一个结构体往后面传,然而这些参数其实和业务是没有多大关系的。通过 context 我们可以很方便的把这些和业务关系不大的参数放到 context 中往后面的方法中传递,在打印日志中可以获取到这些参数统一打印处理。

context 的实现原理

正是由于以上原因,golang 在 1.7 正式引入了 context 的功能,context 的源码去掉注释大概在 300 行左右,其核心是定义了一个 Context 的 interface。

type Context interface {
    Deadline() (deadline time.Time, ok bool)

    Done() <-chan struct{}

    Err() error

    Value(key interface{}) interface{}
}

Deadline() 方法返回的是 context 被取消的截止时间,如果没有设置期限,则 ok 返回为 false;Done() 方法返回一个只读的 channel,当 context 被取消了,监听的 channel 会被关闭,从而可以读取到一个 struct{} 类型的值;Err() 方法返回一个错误,表示 context 被关闭的原因,如果 channel 没有被关闭则返回一个 nil;Value() 方法返回 context 中存储的键值对中 key 对应的值,如果 key 不存在,则返回 nil。 另外一个核心的接口是 canceler 接口,实现了 canceler 接口的 contetx 就可以用来取消 context。

type canceler interface {
    cancel(removeFromParent bool, err error)
    Done() <-chan struct{}
}

源码中有两个 context 实现了 canceler 接口:*cancelCtx 和 *timerCtx。 另外源码中定义了一个 emptyCtx 实现了 Context 接口的功能,我们在构造 context 的时候可以通过 context.Background() 和 context.TODO() 返回这个 emptyCtx,通过获取到这个 context 之后,就可以调用 WithCancel、WithDeadline、WithTimeout、WithValue 来包装相应功能的 context。 因为我们之前说到有些场景主线程中启动多个 groutine,每个 groutine 的超时时间不一样,所以这些 groutine 到达超时时间之后需要能独立退出而不影响到其他的 groutine 的运行,另外主线程取消之后,也需要通知到哥哥 groutine 退出,所以从这种场景其实是一种父子关系的结果,每个父 groutine 可以管理多个子 groutine,每个子 groutine 可以独立管理超时退出,每个子 groutine 又可以创建多个子 groutine,所以在整体结构上来说是一种多叉树的结构,如果我们自己通过代码来实现一颗多叉树,可以通过 slice 或者 map 来实现,eg:

type MultiTreeFromSlice struct {
    RootNode  *Context
    ChildNode []Context
}

type Context interface{}

type MultiTreeFromMap struct {
    RootNode  *Context
    ChildNode map[Context]struct{}
}

golang 源码里面利用 map 的方式来实现这种多叉树的关系:

type cancelCtx struct {
    Context

    mu       sync.Mutex            // protects following fields
    done     chan struct{}         // created lazily, closed by first cancel call
    children map[canceler]struct{} // set to nil by the first cancel call
    err      error                 // set to non-nil by the first cancel call
}

cancelCtx 直接嵌入了 Contex 接口,所以 cancalCtx 也就实现了 Context 的方法,children 字段是一个 canceler 接口为 key 的map,通过获取到这个 canceler 也就获取到了一个可以取消的子 context。 通过 WithValue 方法可以将需要携带的 key, value 参数存入 context,并且在后续的其他方法中通过 context.Value(key) 来获取到 context 中 key 对应的 value。

context 主要功能

实现超时控制

通过 context.WithTimeout 返回一个超时取消的 context,若不调用cancel函数,到了原先创建 contetx时的超时时间,会自动调用 cancel() 函数,在 groutine 中监听 context.Done 的信号即可,通过 context 对使用 channel 来控制一个或多个 groutine 退出的场景改造如下,从结果来看,比使用 channel 来控制简单方便很多。

func main() {

    parentCtx := context.Background()
    ctx, cancel := context.WithTimeout(parentCtx, 3*time.Second)

    go handle(ctx)

    defer cancel()

    time.Sleep(10 * time.Second) // 防止主 groutine 退出
}

// 模拟一个耗时的 rpc 请求
func rpc() (string, error) {
    time.Sleep(4 * time.Second)
D    return "success", nil
}

type result struct {
    Data string
    Err  error
}

func handle(ctx context.Context) {
    r := make(chan result)

    go func() {
        data, err := rpc()
        r <- result{data, err}
    }()

    select {
    case <-ctx.Done():
        log.Println("request is canceled")
        return
    case res := <-r:
        log.Printf("rpc success, data: %v, err: %v", res.Data, res.Err)
    }
}
func main() {
    parentCtx := context.Background()
    ctx, cancel := context.WithTimeout(parentCtx, 4*time.Second) // handle1, handle2 整体超时时间
    defer cancel()

    go handle1(ctx)
    go handle2(ctx)

    time.Sleep(10 * time.Second)
}

func rpc1() (string, error) {
    time.Sleep(3 * time.Second)
    return "success", nil
}

func rpc2() (string, error) {
    time.Sleep(5 * time.Second)
    return "success", nil
}

type result struct {
    Data string
    Err  error
}

func handle1(ctx context.Context) {
    ctx, cancel := context.WithTimeout(ctx, 2*time.Second) // handle1 整体超时时间
    defer cancel()

    r := make(chan result)

    go func() {
        data, err := rpc1()
        r <- result{data, err}
    }()

    select {
    case <-ctx.Done():
        log.Println("handle1 calceled", ctx.Err())
        return
    case res := <-r:
        log.Printf("rpc success, data: %v, err: %v", res.Data, res.Err)
        return
    }
}

func handle2(ctx context.Context) {
    ctx, cancel := context.WithTimeout(ctx, 6*time.Second) // handle2 整体超时时间
    defer cancel()

    r := make(chan result)

    go func() {
        data, err := rpc2()
        r <- result{data, err}
    }()

    select {
    case <-ctx.Done():
        log.Println("handle2 calceled", ctx.Err())
        return
    case res := <-r:
        log.Printf("rpc success, data: %v, err: %v", res.Data, res.Err)
        return
    }
}

需要注意的一点是我们在通过 context.WithTimeout() 方法会返回一个可以取消的 context 和一个cancel方法,虽然在达到指定的超时时间 context 会被取消掉,但是返回的 cancel 方法是不能忽略的,如果我们忽略了这个 cancel 方法 ctx, _ := context.WithTimeout(parentCtx, 4*time.Second),这样写带来的问题是有可能函数在超时时间之前就结束了,但是 context 却没有被取消,造成资源泄露,所以我们一般的写法是执行一个 defer 函数保障 context 是能被取消的。

ctx, cancel := context.WithTimeout(parentCtx, 4*time.Second) // handle1, handle2 整体超时时间
defer cancel()
实现错误取消

通过 context 的 cancel 我们可以实现 errgroup 类似的错误取消功能,如以下示例。实际上,errgroup 实现错误取消的功能也正是通过封装 waitgroup 和 context 的功能来实现的,只不过我们不需要在代码中显示的调用 cancel 方法了。

func main() {
    parentCtx := context.Background()
    ctx, cancel := context.WithCancel(parentCtx) // handle1, handle2 整体超时时间
    defer cancel()

    wg := sync.WaitGroup{}
    wg.Add(2)
    go func() {
        defer wg.Done()
        err := handle1(ctx)
        if err != nil {
            cancel()
        }
    }()

    go func() {
        defer wg.Done()
        err := handle2(ctx)
        if err != nil {
            cancel()
        }
    }()

    wg.Wait()
}

func handle1(ctx context.Context) error {
    select {
    case <-ctx.Done():
        log.Println("handle1 calceled", ctx.Err())
        return fmt.Errorf("handle1 canceled: %v", ctx.Err())
    case <-time.After(1 * time.Second):
        return fmt.Errorf("handle1 time out")
    }
}

func handle2(ctx context.Context) error {
    select {
    case <-ctx.Done():
        log.Println("handle2 calceled", ctx.Err())
        return fmt.Errorf("handle2 canceled: %v", ctx.Err())
    case <-time.After(2 * time.Second):
        return fmt.Errorf("handle2 time out")
    }
}

// 打印结果:
handle2 calceled context canceled
防止 groutine 泄露

我们通过 go 创建协程之后是不能主动控制 goroutine 在指定的时间点退出,所以我们需要对自己创建的 goroutine 负责,不然可能会造成 groutine 泄漏的情况,比如如下整数生成器的例子,一旦调用了这个生成器,goroutine将执行无限循环永远地执行下去。代码中将会泄露一个goroutine。

func main() {
    ch := func() <-chan int {
        ch := make(chan int)
        go func() {
            for i := 0; ; i++ {
                ch <- i
            }
        }()
        return ch
    }()

    for v := range ch {
        fmt.Println(v)
        if v == 5 {
            break
        }
    }

    time.Sleep(5 * time.Second)
}

可以通过在生成器中使用 select 监听 context 的 Done 通道,一旦 context 执行了取消,内部 goroutine将被取消。

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    ch := func(ctx context.Context) <-chan int {
        ch := make(chan int)
        go func() {
            for i := 0; ; i++ {
                select {
                case <-ctx.Done():
                    return
                default:
                    ch <- i
                }
            }
        }()
        return ch
    }(ctx)

    for v := range ch {
        fmt.Println(v)
        if v == 5 {
            cancel()
            break
        }
    }

    time.Sleep(5 * time.Second)
}
实现数据同步

使用 context 实现数据同步最常见的场景是用来传递 traceID 全链路跟踪,通过 context 的 WithValue()方法将需要传递的 traceID 封装之后往后续的方法中传递,后面的方法只需要执行 context.Value 方法就能获取到相应的值。 使用的方法一般是在请求入口处设置 requestID,通过网关服务获取到 requestID 参数之后,调用 WithValue() 方法将 requestID 封装到 context 中,然后传递到后续的方法中或者服务中。

获取 context 里面所有的元素

源代码中 context 中存储 key/value 是通过定义了一个 valueCtx 结构体来实现的,valueCtx 结构体存储了一个 Context 结构用来表示父节点的 Context 对象,如果想往一个 context 中存入 key/value 调用 WithValue() 方法就可以了,整个 valueCte 构成了一颗树结构,和链表有点像,只是它的方向相反:Context 指向它的父节点,链表则指向下一个节点。 在这里插入图片描述 取值过程是一个递归查询的过程,先在当前的 context 中查找是否存在 key,如果没有的话就往上层的 context 中查找,没有最后都没有找到的情况下返回一个 nil。

type valueCtx struct {
    Context
    key, val interface{}
}

func WithValue(parent Context, key, val interface{}) Context {
    if key == nil {
        panic("nil key")
    }
    if !reflectlite.TypeOf(key).Comparable() {
        panic("key is not comparable")
    }
    return &valueCtx{parent, key, val}
}

func (c *valueCtx) Value(key interface{}) interface{} {
    if c.key == key {
        return c.val
    }
    return c.Context.Value(key)
}

Golang 标准库中没有提供一个可以获取存储在 context 中所有的 key/value 的方法,我们可以往context 中存入一个逆向的链表结构,链表中有一个指向父节点的指针,往context 中添加自定义的 key/value 时通过 context.WithValue 方法设置一个固定的 context Key,这样调用 Value 方法之后就能获取到逆向链表的最后一个节点,这样就能获取到整个链路上的 key/value 键值对了。

onst (
    kvCtxKey = "K_KV"
)

type ctxKV struct {
    k   string
    v   string
    pre *ctxKV
}

func ctxAddKV(ctx context.Context, k, v string) context.Context {
    if ctx == nil {
        return nil
    }

    return context.WithValue(ctx, kvCtxKey, &ctxKV{
        k:   k,
        v:   v,
        pre: ctxGetKV(ctx),
    })

}

func ctxGetKV(ctx context.Context) *ctxKV {
    if ctx == nil {
        return nil
    }
    i := ctx.Value(kvCtxKey)
    if i == nil {
        return nil
    }
    if kv, ok := i.(*ctxKV); ok {
        return kv
    }
    return nil
}

func ctxGetV(ctx context.Context, k string) (string, bool) {
    kv := ctxGetKV(ctx)
    if kv == nil {
        return "", false
    }

    for kv != nil {
        if kv.k == k {
            return kv.v, true
        }
        kv = kv.pre
    }

    return "", false
}

func ctxGetAll(ctx context.Context) map[string]string {
    if ctx == nil {
        return nil
    }
    kv := ctxGetKV(ctx)
    if kv == nil {
        return nil
    }

    results := make(map[string]string, 4)
    for kv != nil {
        if _, exist := results[kv.k]; !exist {
            results[kv.k] = kv.v
        }
        kv = kv.pre
    }

    return results
}

context 案例分析 http.Requst

context 在 net/http 包中主要的作用就是来设置请求的超时时间,我们在发送 http 请求都是需要通过 http client 来实现,Go 官方提供的 net/http 包里的 http client 可以通过以下两种方法设置超时: 方法一:Request 结构体里面封装了一个 context.Context 字段,通过对外暴露的 req.WithContext(ctx) 方法可以封装一个支持超时取消的 context 方法二:New 一个自定义 client 设置 Timeout 超时时间

// 方法一
type Request struct {
    ....
    ctx context.Context
}

req := http.NewRequest(....)
ctx, cancel  := context.WithTimeout(context.Background(), time.Second)
defer cancel()
req.WithContext(ctx)
resp, err := http.DefaultClient.Do(req)


// 方法二
cli := http.Client{Timeout: time.Second}
req := http.NewRequest(....)
cli.Do(req)

我们先来看一下方法一是怎样来控制请求超时的:

// WithContext 给 Request 对象赋值
func (r *Request) WithContext(ctx context.Context) *Request {
    if ctx == nil {
        panic("nil context")
    }
    r2 := new(Request)
    *r2 = *r
    r2.ctx = ctx
    r2.URL = cloneURL(r.URL) // legacy behavior; TODO: try to remove. Issue 23544
    return r2
}

func (c *Client) Do(req *Request) (*Response, error) {
    return c.do(req)
}

func (c *Client) do(req *Request) (retres *Response, reterr error) {
    if resp, didTimeout, err = c.send(req, deadline); err != nil {
    }
}

func send(ireq *Request, rt RoundTripper, deadline time.Time) (resp *Response, didTimeout func() bool, err error) {
    resp, err = rt.RoundTrip(req)
}

func (t *Transport) RoundTrip(req *Request) (*Response, error) {
    return t.roundTrip(req)
}

func (t *Transport) roundTrip(req *Request) (*Response, error) {
    ctx := req.Context()
    for {
        // 检查是否关闭了,如果关闭了就直接返回
        select {
        case <-ctx.Done():
            req.closeBody()
            return nil, ctx.Err()
        default:
        }
        
        // 发送请求
        resp, err = pconn.roundTrip(treq)
    }
}

func (pc *persistConn) roundTrip(req *transportRequest) (resp *Response, err error) {
    ctxDoneChan := req.Context().Done()
    for {
        case <-ctxDoneChan:
            pc.t.cancelRequest(req.cancelKey, req.Context().Err())
            cancelChan = nil
            ctxDoneChan = nil
    }
}

通过一步步分析,我们发现 context 的超时控制体现在 Transport(http 连接池) 的 roundTrip 方法在请求发送前和发送过程中会 for 循环里面有一个 select 监听 context.Done() 信号,这样 context 自动超时和主动取消都能监听到从而结束请求。 再看一下方法二的源码调用链路情况:

func (c *Client) do(req *Request) (retres *Response, reterr error) {
    deadline = c.deadline()
    if resp, didTimeout, err = c.send(req, deadline); err != nil {}
}

func (c *Client) deadline() time.Time {
    if c.Timeout > 0 {
        return time.Now().Add(c.Timeout)
    }
    return time.Time{}
}

func send(ireq *Request, rt RoundTripper, deadline time.Time) (resp *Response, didTimeout func() bool, err error) {
    stopTimer, didTimeout := setRequestCancel(req, rt, deadline)
}

func setRequestCancel(req *Request, rt RoundTripper, deadline time.Time) (stopTimer func(), didTimeout func() bool) {
    timer := time.NewTimer(time.Until(deadline))
        go func() {
        select {
        case <-initialReqCancel:
            doCancel()
            timer.Stop()
        case <-timer.C:
            timedOut.setTrue()
            doCancel()
        case <-stopTimerCh:
            timer.Stop()
        }
    }()
}

通过创建 client 的时候指定 Timeout 参数,这种方式是通过起了一个 timer,然后监听 timer 是否到期,如果到期了则执行取消请求。

context 案例分析 gin.Context

gin.Context 结构体是 gin 框架中最重要的一部分,gin.Context 实现了标准库中 context.Context 接口的方法,只不过除了 Value() 方法之外,其他的方法都是返回默认值,所以单纯的 gin.Context 是没有取消功能的,我们最常见的用法是通过 gin.Context 在各个中间件中传递变量,和标准库中的 valueCtx 用来保持 key/value 的方式不同,gin.Context 实现的 Value() 方法是通过封装的 Keys map[string]interface{} 来保持和获取数据的,我们可以在某一个 Middleware 中通过 Set() 设置值,通过Get()、MustGet()、GetString() 等方法在其他的 Middlerware 中获取对应的值。

type Context struct {
    ...
    Request   *http.Request
    // This mutex protect Keys map
    mu sync.RWMutex
    // Keys is a key/value pair exclusively for the context of each request.
    Keys map[string]interface{}
}

func (c *Context) Set(key string, value interface{}) {
    c.mu.Lock()
    if c.Keys == nil {
        c.Keys = make(map[string]interface{})
    }

    c.Keys[key] = value
    c.mu.Unlock()
}

func (c *Context) Get(key string) (value interface{}, exists bool) {
    c.mu.RLock()
    value, exists = c.Keys[key]
    c.mu.RUnlock()
    return
}

// implement context.Context interface
func (c *Context) Deadline() (deadline time.Time, ok bool) {
    return
}

func (c *Context) Done() <-chan struct{} {
    return nil
}

func (c *Context) Err() error {
    return nil
}

func (c *Context) Value(key interface{}) interface{} {
    if key == 0 {
        return c.Request
    }
    if keyAsString, ok := key.(string); ok {
        val, _ := c.Get(keyAsString)
        return val
    }
    return nil
}

这里说明的一点是是在1.6版本之前并发设置和读取值是非线程安全的,对 Map 的操作没有加锁,这样会导致多个线程对同一个值进行设置导致程序 panic,为此在1.6版本中修复了这个问题,Set操作是并发安全的。

context 调用栈太深问题

我们在通过 context.Value() 方法获取对应 key 的 value 的时候,实际上是一个逆向链表的递归查询过程,如果递归的调用栈太深,会导致我们查找一个 key 的效率很差,从而导致 CPU 飙升或者程序栈溢出等情况,所以在使用的时候需要特别注意,以下以一个线上的例子来说明。 以下是抓取的某次上线之后 CPU 告警的 pprof 的截图,可以看到 context.Value 方法的调用栈非常长,查找一个 key 的值递归调用了非常多次 context.Value 的方法,导致占用了整个 CPU 时间段的 83%,这明显是使用 context.WithValue 方法不当导致的。 在这里插入图片描述 最后经过排查是因为我们同学在一个定时器任务中使用的 context 的方式不当造成的,在 for 循环的外层函数中创建了一个 context,但是在 for 循环的里面却复用了外层的 context 并使用 WithValue 方法在 context 中包了一个key/value,这样导致这个 context 层级特别深,在打日志的时候有很多获取context中元数据的操作,每次获取都需要往上层的 context 递归,之后导致调用栈特别长,频繁的调用导致 CPU 飙升。

func Loop() {
    common.SafeGo(func() {
        ctx := context.Background()
        for {
            select {
            case <-cronjobTicker.C:
                ctx = context.WithValue(ctx, TESTKEY, "checkLoop")
                log.Logger(ctx).Infof("Loop start")
    }
}

func Logger(c context.Context) *logrus.Entry {
    ...
    if test, ok := c.Value(TESTKEY).(string); ok {
        fields["test"] = test
    }
    ...
}

context 的最佳实践

context 最佳实践在源码的 pkg 注释中就已经全部体现了,这里做一个翻译:

  1. WithCancel,WithDeadline,WithTimeout 在返回子 contex 的同时会返回一个 CancelFunc,调用 CancelFunc 方法可以取消会取消和父 context 的引用关系,如果 CancelFunc 调用失败会导致子 context 所在的 groutine 在达到设置的超时时间才会返回,会导致 groutine 在一定的时间里面存在泄漏,通过 go vet 工具可以检测出来。
  2. 不要将 context 放到结构体中传递,应该将 context 当做方法的第一个参数显示传递,一般命名为ctx。
  3. 不要传递一个 nil 的 context 即使方法允许,如果不知道传递什么,可以传递一个 context.TODO。这样做的主要原因是避免我们在方法中使用 context 的方法之前还需要判断 context 是否为nil,因为我们在使用 context 的时候都是直接调用其中的方法,如果传递为 nil 调用其中的方法肯定会报 panic。
  4. 使用 context 的 Value 相关方法只应该用于在程序和接口中传递的和请求相关的元数据,不要用它来传递一些可选的参数。
  5. context 是协程安全的,可以放心的在多个 groutine 中进行传递。
// The WithCancel, WithDeadline, and WithTimeout functions take a
// Context (the parent) and return a derived Context (the child) and a
// CancelFunc. Calling the CancelFunc cancels the child and its
// children, removes the parent's reference to the child, and stops
// any associated timers. Failing to call the CancelFunc leaks the
// child and its children until the parent is canceled or the timer
// fires. The go vet tool checks that CancelFuncs are used on all
// control-flow paths.
//
// Programs that use Contexts should follow these rules to keep interfaces
// consistent across packages and enable static analysis tools to check context
// propagation:
//
// Do not store Contexts inside a struct type; instead, pass a Context
// explicitly to each function that needs it. The Context should be the first
// parameter, typically named ctx:
//
//  func DoSomething(ctx context.Context, arg Arg) error {
//      // ... use ctx ...
//  }
//
// Do not pass a nil Context, even if a function permits it. Pass context.TODO
// if you are unsure about which Context to use.
//
// Use context Values only for request-scoped data that transits processes and
// APIs, not for passing optional parameters to functions.
//
// The same Context may be passed to functions running in different goroutines;
// Contexts are safe for simultaneous use by multiple goroutines.
//
// See https://blog.golang.org/context for example code for a server that uses