Golang context

Context背景

随着微服务的发展,大家在写服务的各种响应函数的时候,一定都是将Context作为第一个参数, 很多情况下是为了进行全链路追踪,但是Context的作用不仅这一点。

Context在Go 1.7之前,还不是在Go的标准库中的,而是在golang.org/x/net/context, 后来Golang团队发现这包挺好使,在各个地方都能用,于是就收编进了标准库中。

Context常用在以下场景:

  • 超时控制
  • 上下文控制
  • 在多个gourotine之间交互数据

Context结构

Context的定义如下,省略部分注释。

type Context interface {
    // Deadline returns the time when work done on behalf of this context
    // should be canceled. Deadline returns ok==false when no deadline is
    // set. Successive calls to Deadline return the same results.
    Deadline() (deadline time.Time, ok bool)

    // Done returns a channel that's closed when work done on behalf of this
    // context should be canceled. Done may return nil if this context can
    // never be canceled. Successive calls to Done return the same value.
    // The close of the Done channel may happen asynchronously,
    // after the cancel function returns.
    // ...
    Done() <-chan struct{}

    // If Done is not yet closed, Err returns nil.
    // If Done is closed, Err returns a non-nil error explaining why:
    // Canceled if the context was canceled
    // or DeadlineExceeded if the context's deadline passed.
    // After Err returns a non-nil error, successive calls to Err return the same error.
    // ...
    Err() error

    // Value returns the value associated with this context for key, or nil
    // if no value is associated with key. Successive calls to Value with
    // the same key returns the same result.
    //
    // Use context values only for request-scoped data that transits
    // processes and API boundaries, not for passing optional parameters to
    // functions.
    // ...
    Value(key interface{}) interface{}
}

各个接口的作用如下:

接口 作用
Deadline 返回一个time.Time, 表示当前Context应该结束的时间。如果ok为false,则没有结束时间
Done 返回一个空结构的Channel,如果工作该结束了,则该channel会被关闭。如果nil则该context永远不会被cancel
Err 返回conext被取消的原因。当Err返回不为空时,则以后每次调用都是同样的结果
Value 返回创建context时绑定的数据,可以实现协程间的数据共享

Context是可以传入给多个goroutine的,他是协程安全的。

同时,包中还定义了提供cancel功能所需要实现的接口。这个是cancelCtx和timerCtx实现的接口。

// A canceler is a context type that can be canceled directly. The
// implementations are *cancelCtx and *timerCtx.
type canceler interface {
    cancel(removeFromParent bool, err error)
    Done() <-chan struct{}
}

在标准库中,提供了4个Context的实现,可以来参考使用

实现 结构体 作用
emptyCtx type emptyCtx int 完全是空的Context,所有的方法返回都是nil
cancelCtx type cancelCtx struct {
Context
mu sync.Mutex
done chan struct{}
children map[canceler]struct{}
err error
}
继承自Context,同时也实现了canceler接口
timerCtx type timerCtx struct {
cancelCtx
timer *time.Timer
}
继承自cancelCtx,增加了timeout机制
valueCtx type valueCtx struct {
Context
key, val interface{}
}
集成子Context,提供存储键值对数据的功能

Context的创建

为了更方便的创建Context,包里头定义了Background来作为所有Context的根,它是一个emptyCtx的实例。

var (
    background = new(emptyCtx)
    todo       = new(emptyCtx)
)

func Background() Context {
    return background
}

你可以认为所有的Context是树的结构,Background是树的根,当任一Context被取消的时候,那么继承它的Context都将被回收。

Context使用举例

WithCancel

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
    c := newCancelCtx(parent)
    propagateCancel(parent, &c)
    return &c, func() { c.cancel(true, Canceled) }
}

WithCancel会返回两个返回值,除了返回一个Context外,还会返回一个CancelFunc,用来cancel掉相应的goroutine。

package main

import (
    "context"
    "fmt"
)

func main() {
    // gen generates integers in a separate goroutine and
    // sends them to the returned channel.
    // The callers of gen need to cancel the context once
    // they are done consuming generated integers not to leak
    // the internal goroutine started by gen.
    gen := func(ctx context.Context) <-chan int {
        dst := make(chan int)
        n := 1
        go func() {
            for {
                select {
                case <-ctx.Done():
                    return // returning not to leak the goroutine
                case dst <- n:
                    n++
                }
            }
        }()
        return dst
    }

    ctx, cancel := context.WithCancel(context.Background())
    defer cancel() // cancel when we are finished consuming integers

    for n := range gen(ctx) {
        fmt.Println(n)
        if n == 5 {
            break
        }
    }
}

Output:

1
2
3
4
5

WithDeadline / WithTimeout

WithDeadline和WithTimeout差不多,Deadline接收的是一个指定的时间点,比如16:00,过了这个时间点后,就会cancel掉执行的goroutine, 而Timeout则是接受一个指定的时间值,比如5秒,执行了这么久过后,就会cancel相应的goroutine。

package main

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

func main() {
    d := time.Now().Add(50 * time.Millisecond)
    ctx, cancel := context.WithDeadline(context.Background(), d)

    // Pass a context with a timeout to tell a blocking function that it
    // should abandon its work after the timeout elapses.
    // ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)

    // Even though ctx will be expired, it is good practice to call its
    // cancellation function in any case. Failure to do so may keep the
    // context and its parent alive longer than necessary.
    defer cancel()

    select {
    case <-time.After(1 * time.Second):
        fmt.Println("overslept")
    case <-ctx.Done():
        fmt.Println(ctx.Err()) // prints "context deadline exceeded"
    }

}

output:

context deadline exceeded

WithValue

如果执行时需要携带关键信息,为全链路提供线索,比如接入elk等系统,需要来一个trace_id,那WithValue就非常适合做这个事。

package main

import (
    "context"
    "fmt"
)

func main() {
    type favContextKey string

    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"))

}

output:

found value: Go
key not found: color

Context使用注意事项

  • 在传入Context的时候,不要传入nil,如果拿不准要传入什么,那就传入context.TODO()
  • Context作为全局链路跟踪的时候,要作为第一个参数,且名字按照惯例写为ctx

参考