理解golang的Context设计

Context的使用场景

Context的设计初衷就是为了在多个goroutine之间传递和同步取消信号,以减少资源的消耗和占用,具体有以下使用场景:

  1. 作为请求API的调用方&被调用方时的超时控制,通常用于数据库或者网络连接的超时控制;
  2. 作为各协程之间共用的变量池例如Gin框架中各中间件链式调用时互相传递Context;
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    type Context interface {
    // Deadline returns the time when work done on behalf of this context
    // should be canceled.
    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() <-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.
    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.
    Value(key any) any
    }

Context超时控制的实现原理

通过Done()传递超时信号,并使用Select监听超时信号是否传入,官方注释如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Done is provided for use in select statements:
//
// // Stream generates values with DoSomething and sends them to out
// // until DoSomething returns an error or ctx.Done is closed.
func Stream(ctx context.Context, out chan<- Value) error {
for {
v, err := DoSomething(ctx)
if err != nil {
return err
}
select {
case <-ctx.Done():
return ctx.Err()
case out <- v:
}
}
}

Context的Keys池如何保证线程安全

官方context包下的Context接口内部对Keys没有使用传统锁的机制来保证线程安全,每次对Context的操作都会创建一个新的上下文实例,而不是改变现有的实例(通过WithDeadline(),WithTimeOut()WithCancle()这些方法派生父节点上下文的副本),这样的设计避免了对共享资源的直接修改,从而避免了锁的需要。

1
2
3
4
5
6
7
8
9
10
11
12
func WithValue(parent Context, key, val any) 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}
}

Gin框架实现的Context内部是通过读写锁保证Key-Value变量池的线程安全

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Set is used to store a new key/value pair exclusively for this context.
// It also lazy initializes c.Keys if it was not used previously.
func (c *Context) Set(key string, value any) {
c.mu.Lock()
defer c.mu.Unlock()
if c.Keys == nil {
c.Keys = make(map[string]any)
}

c.Keys[key] = value
}

// Get returns the value for the given key, ie: (value, true).
// If the value does not exist it returns (nil, false)
func (c *Context) Get(key string) (value any, exists bool) {
c.mu.RLock()
defer c.mu.RUnlock()
value, exists = c.Keys[key]
return
}