go context详解|Go主题月

1,513 阅读3分钟

上下文 context

上下文 context.ContextGo 语言中用来设置截止日期、同步信号,传递请求相关值的结构体。上下文与 Goroutine 有比较密切的关系,是 Go 语言中独特的设计,在其他编程语言中我们很少见到类似的概念。

context.ContextGo 语言在 1.7 版本中引入标准库的接口,该接口定义了四个需要实现的方法,其中包括:

  • Deadline — 返回 context.Context 被取消的时间,也就是完成工作的截止日期;
  • Done — 返回一个 Channel,这个 Channel 会在当前工作完成或者上下文被取消后关闭,多次调用 Done 方法会返回同一个 Channel
  • Err — 返回 context.Context 结束的原因,它只会在 Done 方法对应的Channel 关闭时返回非空的值;如果 context.Context 被取消,会返回 Canceled 错误;如果 context.Context 超时,会返回 DeadlineExceeded 错误;
  • Value — 从 context.Context 中获取键对应的值,对于同一个上下文来说,多次调用 Value 并传入相同的 Key 会返回相同的结果,该方法可以用来传递请求特定的数据;
type Context interface {
	Deadline() (deadline time.Time, ok bool)
	Done() <-chan struct{}
	Err() error
	Value(key interface{}) interface{}
}

context 包中提供的 context.Backgroundcontext.TODOcontext.WithDeadlinecontext.WithValue 函数会返回实现该接口的私有结构体。

信号同步

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, duration time.Duration) {
	select {
	case <-ctx.Done():
		fmt.Println("handle", ctx.Err())
	case <-time.After(duration):
		fmt.Println("process request with", duration)
	}
}
Go

默认上下文

context 包中最常用的方法还是 context.Backgroundcontext.TODO,这两个方法都会返回预先初始化好的私有变量 backgroundtodo,它们会在同一个 Go 程序中被复用

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

background 通常用在 main 函数中,作为所有 context 的根节点。

todo 通常用在并不知道传递什么 context的情形。例如,调用一个需要传递 context 参数的函数,你手头并没有其他 context 可以传递,这时就可以传递 todo。这常常发生在重构进行中,给一些函数添加了一个 Context 参数,但不知道要传什么,就用 todo “占个位子”,最终要换成其他 context。

取消

context.WithCancel 函数能够从 context.Context 中衍生出一个新的子上下文并返回用于取消该上下文的函数。一旦我们执行返回的取消函数,当前上下文以及它的子上下文都会被取消,所有的 Goroutine 都会同步收到这一取消信号。

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
	c := newCancelCtx(parent)
	propagateCancel(parent, &c)
	return &c, func() { c.cancel(true, Canceled) }
}
func propagateCancel(parent Context, child canceler) {
	done := parent.Done()
	if done == nil {
		return // 父上下文不会触发取消信号
	}
	select {
	case <-done:
		child.cancel(false, parent.Err()) // 父上下文已经被取消
		return
	default:
	}

	if p, ok := parentCancelCtx(parent); ok {
		p.mu.Lock()
		if p.err != nil {
			child.cancel(false, p.err)
		} else {
			p.children[child] = struct{}{}
		}
		p.mu.Unlock()
	} else {
		go func() {
			select {
			case <-parent.Done():
				child.cancel(false, parent.Err())
			case <-child.Done():
			}
		}()
	}
}
  1. 当 parent.Done() == nil,也就是 parent 不会触发取消事件时,当前函数会直接返回;
  2. 当 child 的继承链包含可以取消的上下文时,会判断 parent 是否已经触发了取消信号;如果已经被取消,child 会立刻被取消;如果没有被取消,child 会被加入 parent 的 children 列表中,等待 parent 释放取消信号;
  3. 当父上下文是开发者自定义的类型、实现了 context.Context 接口并在 Done() 方法中返回了非空的管道时;运行一个新的 Goroutine 同时监听 parent.Done() 和 child.Done() 两个 Channel;在 parent.Done() 关闭时调用 child.cancel 取消子上下文;

值传递

context 包中的 context.WithValue 能从父上下文中创建一个子上下文,传值的子上下文使用 context.valueCtx 类型

func WithValue(parent Context, key, val interface{}) Context {
	if key == nil {
		panic("nil key")
	}
	if !reflectlite.TypeOf(key).Comparable() {
		panic("key is not comparable")
	}
	return &valueCtx{parent, key, val}
}
type valueCtx struct {
	Context
	key, val interface{}
}

func (c *valueCtx) Value(key interface{}) interface{} {
	if c.key == key {
		return c.val
	}
	return c.Context.Value(key)
}

如果还有任何问题或者想了解的内容,可以关注我的公众号超级英雄吉姆,在公众号留言,我看到后第一时间回复。