什么是context

context,中文译作“上下文”,它是 goroutine 的上下文,包含 goroutine 的运行状态、环境、现场等信息。

context 主要用来在 goroutine 之间传递上下文信息,包括:取消信号、超时时间、截止时间、k-v 等。

context.Context 类型的值可以协调多个 groutine 中的代码执行“取消”操作,并且可以存储键值对。最重要的是它是并发安全的。

与它协作的 API 都可以由外部控制执行“取消”操作,例如:取消一个 HTTP 请求的执行。

Context底层解析

整体概览

名称 类型 作用
CancelFunc:func() Func 取消,关闭函数
Context interface 定义了Context的接口方法
cancelCtx struct 可以取消的Context
canceler interface Context 取消接口,定义了两个方法
deadlineExceededError struct 定义了Context返回错误
emptyCtx struct 实现了 Context 接口,其实是个空的 context
timerCtx struct 实现了 Context 接口,有超时取消机制
valueCtx struct 实现了 Context 接口,用于存储 k-v 键值对
Background Func 空的Context,常为根context
TODO Func 返回一个空的 context,常用于 未知context使用场景
WithCancel Func 基于父 context,生成一个可以取消的 子context
WithDeadline Func 基于父 context,创建一个按日期超时取消的子 context
WithTimeout Func 基于父 context,创建一个有 时间超时取消的 子context
WithValue Func 基于父 context,创建一个存储 k-v 对的子 context
init Func 包初始化
newCancelCtx Func 创建一个可以取消的context
parentCancelCtx Func 找到最近的可取消的父节点
propagateCancel Func 向下传递 context 节点间的取消关系
removeChild Func 去掉父节点的子节点

接口

Context

1
2
3
4
5
6
7
8
9
10
type Context interface {
// context 是否会被取消以及自动取消时间(即 deadline)
Deadline() (deadline time.Time, ok bool)
// 当Context被取消或者超时时,则返回被关闭的channel
Done() <-chan struct{}
// 返回取消的原因
Err() error
// 根据key获取value
Value(key interface{}) interface{}
}
  • Context的方法都是幂等,线程安全的,所以连续多次调用同一个方法,得到的结果都是相同的:
    1. Deadline方法返回当前 Context是否有超时机制和被取消的时间,也就是完成工作的截止日期
    2. Done 方法返回一个 Channel,这个 Channel 会在当前工作完成或者上下文被取消之后返回,用与提醒协程关闭,进行收尾工作,尽快退出,多次调用 Done 方法会返回同一个 Channel
    3. Err方法会返回当前 Context 结束的原因,它只会在 Done 返回的 Channel 被关闭时才会返回非空的值:
      1. 如果当前 Context 被取消就会返回 Canceled 错误;
      2. 如果当前 Context 超时就会返回 DeadlineExceeded 错误;
    4. Value方法会从 Context 中返回键对应的值,对于同一个上下文来说,多次调用 Value 并传入相同的 Key 会返回相同的结果,这个功能可以用来传递请求特定的数据;

canceler

1
2
3
4
type canceler interface {
cancel(removeFromParent bool, err error)
Done() <-chan struct{}
}
  • Context取消的接口方法,源码中有两个类型实现了 canceler 接口:*cancelCtx*timerCtx:

    ​ 1. Done 方法返回一个只读的 channel,所有相关函数监听此 channel。一旦 channel 关闭,通过 channel 的“广播机制”,所有监听者都能收到。

    ​ 2. cancel方法当removeFromParent 为 true 时,会将当前节点的 context 从父节点 context中删除,err为取消原因

结构体

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
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
}

func (e *emptyCtx) String() string {
switch e {
case background:
return "context.Background"
case todo:
return "context.TODO"
}
return "unknown empty Context"
}
  • emptyCtx是一个空的 context,永远不会被 取消,没有存储值,也没有 超时。

  • 它被包装成:

    1
    2
    3
    4
    var (
    background = new(emptyCtx)
    todo = new(emptyCtx)
    )
  • 下面两个导出的函数对外公开:

    1
    2
    3
    4
    5
    6
    7
    func Background() Context {
    return background
    }

    func TODO() Context {
    return todo
    }
    1. ``BackgroundTODO方法在某种层面上看其实也只是互为别名,两者没有太大的差别,不过context.Background()是上下文中最顶层的默认值,所有其他的上下文都应该从 context.Background()` 演化出来。
    2. 我们应该只在不确定时使用context.TODO(),在多数情况下如果函数没有上下文作为入参,我们往往都会使用 context.Background() 作为起始的 Context 向下传递。

cancelCtx

1
2
3
4
5
6
7
8
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实现了 canceler 接口。它直接将接口 Context 作为它的一个匿名字段,这样,它就可以被看成一个 Context

  • Done()方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    func (c *cancelCtx) Done() <-chan struct{} {
    c.mu.Lock()
    if c.done == nil { // 懒汉式创建
    c.done = make(chan struct{})
    }
    d := c.done
    c.mu.Unlock()
    return d
    }

    c.done 是“懒汉式”创建,只有调用了 Done() 方法的时候才会被创建

  • StringErr方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    func (c *cancelCtx) Err() error {
    c.mu.Lock()
    err := c.err
    c.mu.Unlock()
    return err
    }

    func (c *cancelCtx) String() string {
    return fmt.Sprintf("%v.WithCancel", c.Context)
    }
  • cancel()方法

    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
    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
    }
    // 设置取消原因
    c.err = err
    // 关闭channel,通知协程关闭
    if c.done == nil {
    c.done = closedchan
    } else {
    close(c.done)
    }
    for child := range c.children {
    // 取消所有子节点
    child.cancel(false, err)
    }
    // 将子节点设为空
    c.children = nil
    c.mu.Unlock()
    if removeFromParent {
    // 从父节点中移除自己
    removeChild(c.Context, c)
    }
    }

    removeChild

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    func removeChild(parent Context, child canceler) {
    p, ok := parentCancelCtx(parent)
    if !ok {
    return
    }
    p.mu.Lock()
    if p.children != nil { // 如果父节点的子节点不为空
    delete(p.children, child) // 将子节点从父节点中移除
    }
    p.mu.Unlock()
    }

    removeChild方法当removeFromParent为true时,将自己从父节点中移除

    cancel() 方法的功能:

    1. 关闭 channel:c.done;
     2. 递归地取消它的所有子节点;
     3. 从父节点从删除自己。
     4. 通过关闭 `channel`,将取消信号传递给了它的所有子节点。`goroutine `接收到取消信号的方式就是 select 语句中的读 c.done 被选中
    

timerCtx

1
2
3
4
5
6
type timerCtx struct {
cancelCtx
timer *time.Timer // 定时器

deadline time.Time // 过期时间
}

timerCtx 基于 cancelCtx,只是多了一个 time.Timer 和一个 deadlineTimer 会在 deadline 到来时,自动取消 context

  • cancel 方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    func (c *timerCtx) cancel(removeFromParent bool, err error) {
    // 直接调用 cancelCtx 的取消方法
    c.cancelCtx.cancel(false, err)
    if removeFromParent {
    // 从父节点中删除子节点
    removeChild(c.cancelCtx.Context, c)
    }
    c.mu.Lock()
    if c.timer != nil {
    // 关掉定时器,这样,在deadline 到来时,不会再次取消
    c.timer.Stop()
    c.timer = nil
    }
    c.mu.Unlock()
    }
  • Deadline方法

    1
    2
    3
    func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
    return c.deadline, true
    }

    获取timerCtx超时时间

valueCtx

1
2
3
4
type valueCtx struct {
Context
key, val interface{}
}
  • StringValue方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // Context信息
    func (c *valueCtx) String() string {
    return fmt.Sprintf("%v.WithValue(%#v, %#v)", c.Context, c.key, c.val)
    }
    // 获取指定的键值
    func (c *valueCtx) Value(key interface{}) interface{} {
    if c.key == key {
    return c.val
    }
    return c.Context.Value(key)
    }

和链表有点像,只是它的方向相反:Context 指向它的父节点,链表则指向下一个节点。通过 WithValue 函数,可以创建层层的 valueCtx,存储 goroutine 间可以共享的变量。

1
2
3
4
5
6
func (c *valueCtx) Value(key interface{}) interface{} {
if c.key == key {
return c.val
}
return c.Context.Value(key)
}

它会顺着链路一直往上找,比较当前节点的 key
是否是要找的 key,如果是,则直接返回 value。否则,一直顺着 context 往前,最终找到根节点(一般是 emptyCtx),直接返回一个 nil。所以用 Value 方法的时候要判断结果是否为 nil。

WithValue 创建 context 节点的过程实际上就是创建链表节点的过程。两个节点的 key 值是可以相等的,但它们是两个不同的 context 节点。查找的时候,会向上查找到最后一个挂载的 context 节点,也就是离得比较近的一个父节点 context。所以,整体上而言,用 WithValue 构造的其实是一个低效率的链表。

因为查找方向是往上走的,所以,父节点没法获取子节点存储的值,子节点却可以获取父节点的值。

因为它是链表查询的方式,所有时间复制度为O(n),所以不推荐大量使用

函数

WithCancel

1
2
3
4
5
6
7
8
9
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
c := newCancelCtx(parent)
propagateCancel(parent, &c)
return &c, func() { c.cancel(true, Canceled) }
}

func newCancelCtx(parent Context) cancelCtx {
return cancelCtx{Context: parent}
}
  • 传入一个父的Context,返回一个新子Context CancelFunc取消方法
  • WithCancel 函数返回的 CancelFunc 被调用或者是父节点的 done channel 被关闭(父节点的 CancelFunc 被调用),此 context(子节点) 的 done channel 也会被关闭。

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
func propagateCancel(parent Context, child canceler) {

if parent.Done() == nil { // 父节点是个空节点
return // parent is never canceled
}
// 找到可以取消的父 context
if p, ok := parentCancelCtx(parent); ok {
p.mu.Lock()
if p.err != nil {
// 父节点已经被取消了,本节点(子节点)也要取消
child.cancel(false, p.err)
} else {
// 父节点未取消
if p.children == nil {
p.children = make(map[canceler]struct{})
}
// "挂到"父节点上
p.children[child] = struct{}{}
}
p.mu.Unlock()
} else {
// 如果没有找到可取消的父 context。新启动一个协程监控父节点或子节点取消信号
go func() {
select {
case <-parent.Done():
child.cancel(false, parent.Err())
case <-child.Done():
}
}()
}
}

propagateCancel方法的作用就是向上寻找可以“挂靠”的“可取消”的 context,并且“挂靠”上去。这样,调用上层 cancel 方法的时候,就可以层层传递,将那些挂靠的子 context 同时“取消”。

这里着重解释下为什么会有 else 描述的情况发生。else 是指当前节点 context 没有向上找到可以取消的父节点,那么就要再启动一个协程监控父节点或者子节点的取消动作。

  • select 语句里的两个 case说明

    1
    2
    3
    4
    5
    6
    7
    go func() {
    select {
    case <-parent.Done():
    child.cancel(false, parent.Err())
    case <-child.Done():
    }
    }()
    1. 第一个 case 说明当父节点取消,则取消子节点。如果去掉这个 case,那么父节点取消的信号就不能传递到子节点。
    2. 第二个 case 是说如果子节点自己取消了,那就退出这个 select,父节点的取消信号就不用管了。如果去掉这个 case,那么很可能父节点一直不取消,这个 goroutine 就泄漏了。当然,如果父节点取消了,就会重复让子节点取消,不过,这也没什么影响嘛。

parentCancelCtx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func parentCancelCtx(parent Context) (*cancelCtx, bool) {
for {
switch c := parent.(type) {
case *cancelCtx:
return c, true
case *timerCtx:
return &c.cancelCtx, true
case *valueCtx:
parent = c.Context // 循环获取,直到是可取消的父节点,或者根节点,或者自定义的节点
default:
return nil, false
}
}
}

这里只会识别三种 Context 类型:*cancelCtx*timerCtx*valueCtx。若是把 Context 内嵌到一个类型里,就识别不出来了。

WithTimeout

1
2
3
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}

WithTimeout 函数直接调用了 WithDeadline,传入的 deadline 是当前时间加上 timeout 的时间,也就是从现在开始再经过 timeout 时间就算超时

WithDeadline

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
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) {
if cur, ok := parent.Deadline(); ok && cur.Before(deadline) {
// 如果父节点 context 的 deadline 早于指定时间。直接构建一个可取消的 context。
// 原因是一旦父节点超时,自动调用 cancel 函数,子节点也会随之取消。
// 所以不用单独处理子节点的计时器时间到了之后,自动调用 cancel 函数
return WithCancel(parent)
}

// 构建 timerCtx
c := &timerCtx{
cancelCtx: newCancelCtx(parent),
deadline: deadline,
}
// 挂靠到父节点上
propagateCancel(parent, c)

// 计算当前距离 deadline 的时间
d := time.Until(deadline)
if d <= 0 {
// 直接取消
c.cancel(true, DeadlineExceeded) // deadline has already passed
return c, func() { c.cancel(true, Canceled) }
}
c.mu.Lock()
defer c.mu.Unlock()
if c.err == nil {
// 到达时间后,timer 会自动调用 cancel 函数。自动取消
c.timer = time.AfterFunc(d, func() {
c.cancel(true, DeadlineExceeded)
})
}
return c, func() { c.cancel(true, Canceled) }
}

WithValue

1
2
3
4
5
6
7
8
9
func WithValue(parent Context, key, val interface{}) Context {
if key == nil {
panic("nil key")
}
if !reflect.TypeOf(key).Comparable() {
panic("key is not comparable")
}
return &valueCtx{parent, key, val}
}

创建valueCtx

Context使用案例

共享数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
"context"
"fmt"
)

func main() {
ctx := context.Background()
process(ctx)

ctx2 := context.WithValue(ctx, "traceId", "2019")
process(ctx2)
}

func process(ctx context.Context) {
traceId, ok := ctx.Value("traceId").(string)
if ok {
fmt.Printf("process trace_id=%s\n", traceId)
} else {
fmt.Printf("process no trace_id\n")
}
}

超时取消和协程安全关闭

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func main() {
// 设置一个过期时间为1秒的context
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
//go handle(ctx, 500*time.Millisecond)
go handle(ctx, 1500*time.Millisecond)

select {
case <-ctx.Done(): // 过期关闭context
fmt.Println("main", ctx.Err())
}
}

func handle(ctx context.Context, duration time.Duration) {
select {
case <-ctx.Done():
fmt.Println("handle", ctx.Err())

case <-time.After(duration): // select阻塞时间
fmt.Println("process request with", duration)
}
}

Context使用建议

  1. 不要将 Context 塞到结构体里。直接将 Context 类型作为函数的第一参数,而且一般都命名为 ctx。
  2. 不要向函数传入一个 nil 的 context,如果你实在不知道传什么,标准库给你准备好了一个 context:todo。
  3. 不要把本应该作为函数参数的类型塞到 context 中,context 存储的应该是一些共同的数据。例如:登陆的 session、cookie 等。
  4. 同一个 context 可能会被传递到多个 goroutine,别担心,context 是并发安全的。