context.Context
其实是一个接口,提供了以下4种方法:
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
context
中携带值是非常少见的,其一般在跨程序的API
中使用,并且该值的作用域在结束时终结。key
必须是访问安全的,因为可能有多个协程同时访问它。一种常见的策略是在context
中存储授权相关的值,这些鉴权不会影响程序的核心逻辑。
在Goroutine
构成的树形结构中对信号进行同步以减少计算资源的浪费是context.Context
的最大作用。Go
服务的每一个请求都是通过单独的Goroutine
处理的,HTTP/RPC
请求的处理器会启动新的Goroutine
访问数据库和其他服务。
如下图所示,我们可能会创建多个Goroutine
来处理一次请求,而context.Context
的作用是在不同Goroutine
之间同步请求特定数据、取消信号以及处理请求的截止日期。
每一个context.Context
都会从最顶层的Goroutine
一层一层传递到最下层。context.Context
可以在上层Goroutine
执行出现错误时,将信号及时同步给下层。
如上图所示,当最上层的Goroutine
因为某些原因执行失败时,下层的Goroutine
由于没有接收到这个信号所以会继续工作;但是当我们正确地使用context.Context
时,就可以在下层及时停掉无用的工作以减少额外的资源消耗:
我们可以通过一个代码片段了解context.Context
是如何对信号进行同步的。在这段代码中,我们创建了一个过期时间为1s的上下文,并向上下文传入handle
函数,该方法会使用500ms的时间处理传入的请求:
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
go handle(ctx, 500*time.Millisecond)
select {
case <-ctx.Done():
fmt.Println("main", ctx.Err())
}
}
func handle(ctx context.Context, d time.Duration) {
select {
case <-ctx.Done():
fmt.Println("handle", ctx.Err())
case <-time.After(d):
fmt.Println("process request with", d)
}
}
因为过期时间大于处理时间,所以我们有足够的时间处理该请求,运行上述代码会打印出下面的内容:
$ go run main.go
process request with 500ms
main context deadline exceeded
handle
函数没有进入超时的select
分支,但是main
函数的select
却会等待context.Context
超时并打印出main context deadline exceeded
。
如果我们将处理请求时间增加值1500ms,整个程序都会因为上下文的过期而被终止:
$ go run main.go
handle context deadline exceeded
main context deadline exceeded
相信这两个例子能够帮助各位理解context.Context
的使用方法和设计原理:多个Goroutine
同时订阅ctx.Done()
管道中的消息,一旦接收到信号就会立刻停止当前正在执行的工作。
context
包中最常用的方法还是context.Background
、context.TODO
,这两个方法都会返回预先初始化好的私有变量background
和todo
,它们会在同一个Go
程序中被复用:
// Background returns a non-nil, empty Context. It is never canceled, has no
// values, and has no deadline. It is typically used by the main function,
// initialization, and tests, and as the top-level Context for incoming
// requests.
func Background() Context {
return background
}
// TODO returns a non-nil, empty Context. Code should use context.TODO when
// it's unclear which Context to use or it is not yet available (because the
// surrounding function has not yet been extended to accept a Context
// parameter).
func TODO() Context {
return todo
}
这两个私有变量都是通过new(emptyCtx)
语句初始化的,它们是指向私有结构体context.emptyCtx
的指针,这是最简单、最常用的上下文类型:
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
// An emptyCtx is never canceled, has no values, and has no deadline. It is not
// struct{}, since vars of this type must have distinct addresses.
type emptyCtx int
func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
return
}
func (*emptyCtx) Done() <-chan struct{} {
return nil
}
func (*emptyCtx) Err() error {
return nil
}
func (*emptyCtx) Value(key interface{}) interface{} {
return nil
}
从上述代码中,我们不难看出context.emptyCtx
通过空方法实现了context.Context
接口中所有方法,它没有任何功能。
从源码来看,context.Background
和context.TODO
也只是互为别名,没有太大差别,只是在使用和语义上稍有不同:
在多数情况下,如果当前函数没有上下文作为入参,我们都会使用context.Background
作为起始的上下文向下传递。
context.WithCancel
函数能够从context.Context
中衍生出来的一个新的子上下文并返回用于取消该上下文的函数。一旦我们执行返回的取消函数,当前上下文以及它的子上下文都会被取消,所有的Goroutine
都会同步收到这一取消信号。
我们直接从context.WithCancel
函数的实现来看它到底做了什么:
// WithCancel returns a copy of parent with a new Done channel. The returned
// context's Done channel is closed when the returned cancel function is called
// or when the parent context's Done channel is closed, whichever happens first.
//
// Canceling this context releases resources associated with it, so code should
// call cancel as soon as the operations running in this Context complete.
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
if parent == nil {
panic("cannot create context from nil parent")
}
c := newCancelCtx(parent)
propagateCancel(parent, &c)
return &c, func() { c.cancel(true, Canceled) }
}
// propagateCancel arranges for child to be canceled when parent is.
func propagateCancel(parent Context, child canceler) {
done := parent.Done()
if done == nil {
return // parent is never canceled
}
select {
case <-done:
// parent is already canceled
child.cancel(false, parent.Err())
return
default:
}
if p, ok := parentCancelCtx(parent); ok {
p.mu.Lock()
if p.err != nil {
// parent has already been canceled
child.cancel(false, p.err)
} else {
if p.children == nil {
p.children = make(map[canceler]struct{})
}
p.children[child] = struct{}{}
}
p.mu.Unlock()
} else {
atomic.AddInt32(&goroutines, +1)
go func() {
select {
case <-parent.Done():
child.cancel(false, parent.Err())
case <-child.Done():
}
}()
}
}
上述函数总共与父上下文相关的三种不同的情况:
context.propagateCancel
的作用是在parent
和child
之间同步取消和结束的信号,保证在parent
被取消时,child
也会收到对应的信号,不会出现状态不一致的情况。
context.cancelCtx
实现的几个接口方法没有太多值得分析的地方,该结构体最重要的方法是context.cancelCtx.cancel
,该方法会关闭上下文中的Channel
并向所有的子上下文同步取消信号:
// cancel closes c.done, cancels each of c's children, and, if
// removeFromParent is true, removes c from its parent's children.
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
if err == nil {
panic("context: internal error: missing cancel error")
}
c.mu.Lock()
if c.err != nil {
c.mu.Unlock()
return // already canceled
}
c.err = err
d, _ := c.done.Load().(chan struct{})
if d == nil {
c.done.Store(closedchan)
} else {
close(d)
}
for child := range c.children {
// NOTE: acquiring the child's lock while holding parent's lock.
child.cancel(false, err)
}
c.children = nil
c.mu.Unlock()
if removeFromParent {
removeChild(c.Context, c)
}
}
除了context.WithCancel
之外,context
包中的另外两个函数context.WithDeadline
和context.WithTimeout
也都能创建可以被取消的计时器上下文context.timerCtx
:
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
if parent == nil {
panic("cannot create context from nil parent")
}
if cur, ok := parent.Deadline(); ok && cur.Before(d) {
// The current deadline is already sooner than the new one.
return WithCancel(parent)
}
c := &timerCtx{
cancelCtx: newCancelCtx(parent),
deadline: d,
}
propagateCancel(parent, c)
dur := time.Until(d)
if dur <= 0 {
c.cancel(true, DeadlineExceeded) // deadline has already passed
return c, func() { c.cancel(false, Canceled) }
}
c.mu.Lock()
defer c.mu.Unlock()
if c.err == nil {
c.timer = time.AfterFunc(dur, func() {
c.cancel(true, DeadlineExceeded)
})
}
return c, func() { c.cancel(true, Canceled) }
}
context.WithDeadline
在创建context.timerCtx
的过程中判断了父上下文的截止日期与当前日期,并通过timer.AfterFunc
创建定时器,当时间超过了截至日期后会调用context.timerCtx.cancel
同步取消信号,
context.timerCtx
内部不仅通过嵌入context.cancelCtx
结构体继承了相关的变量和方法,还通过持有的定时器timer
和截止时间deadline
实现了定时取消的功能:
// A timerCtx carries a timer and a deadline. It embeds a cancelCtx to
// implement Done and Err. It implements cancel by stopping its timer then
// delegating to cancelCtx.cancel.
type timerCtx struct {
cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}
func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
return c.deadline, true
}
func (c *timerCtx) String() string {
return contextName(c.cancelCtx.Context) + ".WithDeadline(" +
c.deadline.String() + " [" +
time.Until(c.deadline).String() + "])"
}
func (c *timerCtx) cancel(removeFromParent bool, err error) {
c.cancelCtx.cancel(false, err)
if removeFromParent {
// Remove this timerCtx from its parent cancelCtx's children.
removeChild(c.cancelCtx.Context, c)
}
c.mu.Lock()
if c.timer != nil {
c.timer.Stop()
c.timer = nil
}
c.mu.Unlock()
}
context.timerCtx.cancel
方法不仅调用了context.cancelCtx.cancel
,还会停止持有的定时器减少不必要的资源浪费。
在最后我们需要了解如何使用上下文传值,context
包中的context.WithValue
能从父上下文中创建一个子上下文,传值的子上下文使用context.valueCtx
类型:
func WithValue(parent Context, key, val interface{}) Context {
if parent == nil {
panic("cannot create context from nil parent")
}
if key == nil {
panic("nil key")
}
if !reflectlite.TypeOf(key).Comparable() {
panic("key is not comparable")
}
return &valueCtx{parent, key, val}
}
context.valueCtx
结构体会将除了Value
之外的Err
、Deadline
等方法代理到父上下文中,它只会响应context.valueCtx.Value
方法,该方法的实现也很简单:
// A valueCtx carries a key-value pair. It implements Value for that key and
// delegates all other calls to the embedded Context.
type valueCtx struct {
Context
key, val interface{}
}
// stringify tries a bit to stringify v, without using fmt, since we don't
// want context depending on the unicode tables. This is only used by
// *valueCtx.String().
func stringify(v interface{}) string {
switch s := v.(type) {
case stringer:
return s.String()
case string:
return s
}
return "<not Stringer>"
}
func (c *valueCtx) String() string {
return contextName(c.Context) + ".WithValue(type " +
reflectlite.TypeOf(c.key).String() +
", val " + stringify(c.val) + ")"
}
func (c *valueCtx) Value(key interface{}) interface{} {
if c.key == key {
return c.val
}
return c.Context.Value(key)
}
如果context.valueCtx
中存储的键值对与context.valueCtx.Value
方法中传入的参数不匹配,就会从父上下文中查找该键对应的值直到某个父上下文中返回nil
或者查找到对应的值。
Go
语言中的context.Context
的主要作用还是在多个Goroutine
组成的树中同步取消信号以减少对资源的消耗和占用,虽然它也有传值的功能,但这个功能我们很少用到。
在真正使用传值的功能时我们也应该非常谨慎,使用context.Context
传递请求的所有参数是一种非常差的设计,比较常见的使用场景是传递请求对应用户的认证令牌以及用于进行分布式追踪的请求ID
。