前言

从转go语言开发开始,会发现在日常开发时,很多方法的入参的第一个参数都是context.Context。同时我们在代码逻辑中绝大部分情况下,不会对context继续任何处理。今天就来从源码的角度弄清楚Context是如何实现的及在程序中起到什么样的作用。

Context源码解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// Package context defines the Context type, which carries deadlines,
// cancellation signals, and other request-scoped values across API boundaries
// and between processes.
//
// Incoming requests to a server should create a Context, and outgoing
// calls to servers should accept a Context. The chain of function
// calls between them must propagate the Context, optionally replacing
// it with a derived Context created using WithCancel, WithDeadline,
// WithTimeout, or WithValue. When a Context is canceled, all
// Contexts derived from it are also canceled.

// 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.

从Context包注释上可以看到,golang中定义context可以在函数调用链上,进程间以及服务间传递上下文信息,同时其中可以通过WithCancel,WithDeadline,WithTimeout,WithValue来增加上下文传递信息。同时当context触发cancel的时候,整个context链(为什么会说链,后面会介绍)都会被取消。 Context在gonlang中实现是协程间并发安全的。

使用Context时需要注意

  1. 在函数和方法入参需要传入Context时,如果调用链上有没有传入Context信息,不要使用nil来向下传递,使用 context.TODO() 或者context.Background()来进行context的传递
  2. 不要将Context作为其他Struct的属性定义,相反将context显示的通过函数和方法的首个参数继续传递
  3. 不要乱用Context将一些可选参数通过Context进行传递
1
2
3
4
5
6
7
8
9
10
11
12
13
// A Context carries a deadline, a cancellation signal, and other values across
// API boundaries.
//
// Context's methods may be called by multiple goroutines simultaneously.
type Context interface {
Deadline() (deadline time.Time, ok bool)

Done() <-chan struct{}

Err() error

Value(key any) any
}

Context需要实现Deadline,Done,Err,Value四个方法:

  1. Deadline方法返回任务执行的结束时间以及是否设置了deadline信息,当设置了deadline同时执行时间到达deadline时间,则会出发cancel动作
  2. Done方法作用是在任务完成时调用,对整个context链进行取消操作;不同类型的context实现方式不同
  3. Err方法返回contenxt操作时出现的错误信息
  4. Value方法是遍历整个Context链来获取传入key对应的值信息

几种类型的Context

Context的几种实现

emptyCtx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 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 any) any {
return nil
}

func (e *emptyCtx) String() string {
switch e {
case background:
return "context.Background"
case todo:
return "context.TODO"
}
return "unknown empty Context"
}

emptyCtx是最基础的Context类型,从源码中可以看到emptyCtx是实现了了Context接口的int类型,并且可以看到日常使用的Context的最基础节点Background和TODO均为emptyCtx类型

cancelCtx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 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{}
}

// A cancelCtx can be canceled. When canceled, it also cancels any children
// that implement canceler.
type cancelCtx struct {
Context

mu sync.Mutex // protects following fields
done atomic.Value // of 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
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 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)
}
}

propagateCancel方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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():
}
}()
}
}

timerCtx

1
2
3
4
5
6
7
8
9
// 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
}

valueCtx

1
2
3
4
5
6
// 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 any
}

Context的链式结构

从context的源码中可看到,Context其实是一个链表结构,使用的Context在每次调用WithXXX方法后,使用头插法的方式将新Context作为Context链的队首进行返回

总结

延展

any类型

1
2
// any is an alias for interface{} and is equivalent to interface{} in all ways.
type any = interface{}