Golang原理剖析(context、context面试与分析)

文章目录

context是什么

context 是 Go 语言在 1.7 版本中引入的标准库,用于在 API 调用链和多个 goroutine 之间传递取消信号、超时/截止时间以及请求范围内的元数据 。它通常用于实现父 goroutine 对下层 goroutine 的取消控制 ,而非通用的数据通信机制。context 本身是并发安全的,其取消通知机制底层基于 channel 实现广播,并通过 sync.Mutex 保证状态访问的并发安全

context的底层实现

与context相关的源码基本都在src/context/context.go中,我们通过源码来看看一下,context的底层究竟做了些什么

context在底层实现上其实用到了2个接口,对这个接口的4种实现,以及提供了6个方法

下面我们将逐一解读这几个结构及其实现方法。

接口说明

context接口

首先还是回顾一下context接口,context的接口定义如下:

go 复制代码
// 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 returns the time when work done on behalf of this context
	// should be canceled. Deadline returns ok==false when no deadline is
	// set. Successive calls to Deadline return the same results.
	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 may return nil if this context can
	// never be canceled. Successive calls to Done return the same value.
	// The close of the Done channel may happen asynchronously,
	// after the cancel function returns.
	//
	// WithCancel arranges for Done to be closed when cancel is called;
	// WithDeadline arranges for Done to be closed when the deadline
	// expires; WithTimeout arranges for Done to be closed when the timeout
	// elapses.
	//
	// 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:
	//  		}
	//  	}
	//  }
	//
	// See https://blog.golang.org/pipelines for more examples of how to use
	// a Done channel for cancellation.
	Done() <-chan struct{}

	// If Done is not yet closed, Err returns nil.
	// If Done is closed, Err returns a non-nil error explaining why:
	// DeadlineExceeded if the context's deadline passed,
	// or Canceled if the context was canceled for some other reason.
	// After Err returns a non-nil error, successive calls to Err return the same error.
	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.
	//
	// Use context values only for request-scoped data that transits
	// processes and API boundaries, not for passing optional parameters to
	// functions.
	//
	// A key identifies a specific value in a Context. Functions that wish
	// to store values in Context typically allocate a key in a global
	// variable then use that key as the argument to context.WithValue and
	// Context.Value. A key can be any type that supports equality;
	// packages should define keys as an unexported type to avoid
	// collisions.
	//
	// Packages that define a Context key should provide type-safe accessors
	// for the values stored using that key:
	//
	// 	// Package user defines a User type that's stored in Contexts.
	// 	package user
	//
	// 	import "context"
	//
	// 	// User is the type of value stored in the Contexts.
	// 	type User struct {...}
	//
	// 	// key is an unexported type for keys defined in this package.
	// 	// This prevents collisions with keys defined in other packages.
	// 	type key int
	//
	// 	// userKey is the key for user.User values in Contexts. It is
	// 	// unexported; clients use user.NewContext and user.FromContext
	// 	// instead of using this key directly.
	// 	var userKey key
	//
	// 	// NewContext returns a new Context that carries value u.
	// 	func NewContext(ctx context.Context, u *User) context.Context {
	// 		return context.WithValue(ctx, userKey, u)
	// 	}
	//
	// 	// FromContext returns the User value stored in ctx, if any.
	// 	func FromContext(ctx context.Context) (*User, bool) {
	// 		u, ok := ctx.Value(userKey).(*User)
	// 		return u, ok
	// 	}
	Value(key any) any
}

接口提供了四个方法:

  • Deadline:返回 context.Context 被取消的时间,即截止时间;如果未设置截止日期,Deadline 函数返回 ok==false。连续调用 Deadline 函数会返回相同的结果。

  • Done:返回一个 Channel,当 Context 被取消或者到达截止时间,这个 Channel 就会被关闭,表示 context 结束,多次调用 Done 方法返回的 channel 是同一个

  • Err:返回 context.Context 结束的原因

  • Value:从 context.Context 中获取键对应的值 ,类似于map的get方法,对于同一个 context,多次调用 Value 并传入相同的 Key 会返回相同的结果,如果没有对应的 key,则返回 nil,键值对是通过 WithValue 方法写入

canceler接口

canceler接口的源码定义如下:

go 复制代码
// A canceler is a context type that can be canceled directly. The
// implementations are *cancelCtx and *timerCtx.
type canceler interface {
	cancel(removeFromParent bool, err, cause error)   // 创建cancel接口实例的goroutine 调用cancel方法通知被创建的goroutine退出
	Done() <-chan struct{}		 // 返回一个channel,后续被创建的goroutine通过监听这个channel的信号来完成退出
}

canceler接口主要用于取消方法的实现,如果一个实例既实现了 context 接口又实现了 canceler 接口,那么这个 context 就是可以被取消的,比如 cancelCtx 和 timerCtx。如果仅仅只是实现了 context 接口,而没有实现 canceler,就是不可取消的,比如 emptyCtx 和 valueCtx

cancel() 会触发 Done() 关闭

更准确的关系是:

  1. 上游(创建者/控制者)调用 cancel()(或超时到期、deadline 到期)
  2. 该 context 的 Done() 返回的 channel 会被 close
  3. 下游 goroutine 里 select 监听 <-ctx.Done(),一旦可读(channel 被关闭),就退出/回收资源
go 复制代码
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

go func() {
    select {
    case <-ctx.Done():
        // 收到取消信号:退出、清理
        return
    }
}()

// 某个条件满足时触发取消
cancel()

context实现

在context包下对context接口有四种基本的实现,即 emptyCtx、cancelCtx、timerCtx、valueCtx。

emptyCtx

首先看一下 emptyCtx 这个最基本的实现,emptyCtx 虽然实现了 context 接口,但是不具备任何功能,因为实现很简单,基本都是直接返回空值。也就是说其实是一个 "啥也不干" 的 Context;它通常用于创建 root Context,标准库中 context.Background() 和 context.TODO() 返回的就是这个 emptyCtx。

虽然 emptyCtx 没有任何功能,但他还是有作用的,一般用它作为根context来派生出有实际用处的context。要想创建有实际功能的context,要使用后续提供的一系列with方法来派生出新的context。

emptyCtx 的相关源码:

go 复制代码
// An emptyCtx is never canceled, has no values, and has no deadline.
// It is the common base of backgroundCtx and todoCtx.
type emptyCtx struct{}

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
}

可以看到 emptyCtx 的实现没有做任何操作,就是一个空结构体的类型。这个空的 emptyCtx 会在两个创建根context的函数被用到。

Background / TODO

context.Background(): 该方法用于创建 root Context,且不可取消

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

// 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 todoCtx{}
}

而这里 background 和 todo 其实就是返回一个 emptyCtx:

go 复制代码
type backgroundCtx struct{ emptyCtx }

type todoCtx struct{ emptyCtx }

在写代码的时候,我们会调用这两个函数其实 Background() 函数或者 TODO() 函数创建最顶层的 context 其实就是获取一个 emptyCtx。

backgroundCtx 和 todoCtx 是嵌入了 emptyCtx 的结构体类型,用组合实现"继承式复用":复用 emptyCtx 的 Context 行为,再用不同的类型名区分 Background() 和 TODO() 的语义。

cancelCtx

cancelCtx 结构定义如下:

go 复制代码
// A cancelCtx can be canceled. When canceled, it also cancels any children
// that implement canceler.
type cancelCtx struct {
	Context	// 组合了一个Context ,所以cancelCtx 一定是context接口的一个实现
	        // 互斥锁,用于保护以下三个字段​ // value是一个chan struct{}类型,原子操作做锁优化
	mu       sync.Mutex            // protects following fields
	done     atomic.Value          // of chan struct{}, created lazily, closed by first cancel call
	// key是一个取消接口的实现,map其实存储的是当前canceler接口的子节点,当前context被取消时,会遍历子节点发送取消信号
	children map[canceler]struct{} // set to nil by the first cancel call
	// context被取消的原因
	err      atomic.Value          // set to non-nil by the first cancel call
	cause    error                 // set to non-nil by the first cancel call
}

// A canceler is a context type that can be canceled directly. The
// implementations are *cancelCtx and *timerCtx.
type canceler interface {
	cancel(removeFromParent bool, err, cause error)
	Done() <-chan struct{}
}

嵌入 Context 是为了把 cancelCtx 做成"对父 Context 的装饰器(Decorator)":在不破坏原有 Context 行为的基础上,额外提供取消能力,并且仍然可以当作 context.Context 使用。"嵌入接口可以把其他方法委托出去,从而只覆盖少数方法"

go 复制代码
// 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     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
}

func (c *cancelCtx) Value(key interface{}) interface{} {
	if key == &cancelCtxKey {
		return c
	}
	return c.Context.Value(key)
}

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
}

func (c *cancelCtx) Err() error {
	c.mu.Lock()
	err := c.err
	c.mu.Unlock()
	return err
}

type stringer interface {
	String() string
}

func contextName(c Context) string {
	if s, ok := c.(stringer); ok {
		return s.String()
	}
	return reflectlite.TypeOf(c).String()
}

func (c *cancelCtx) String() string {
	return contextName(c.Context) + ".WithCancel"
}

// 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
	if c.done == nil {
		c.done = closedchan
	} else {
		close(c.done)
	}
	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)
	}
}

下面看一下其各个方法的具体实现,首先看一下 Done() 方法:

Done()方法

go 复制代码
func (c *cancelCtx) Done() <-chan struct{} {
	d := c.done.Load()
	if d != nil {
		return d.(chan struct{})
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	d = c.done.Load()
	if d == nil {
		d = make(chan struct{})
		c.done.Store(d)
	}
	return d.(chan struct{})
}

代码很简单,其实就是采用"懒汉模式"创建一个 struct{} 类型的管道返回 ,从类型可以看出这个 channel 是只读的,不能往里面写数据 ,所以应该避免直接读取这个 channel,会发生阻塞。所以在使用上要配合 select 来非阻塞读取;由于是只读的,所以只有在一种情况下会读到值,那就是关闭这个 channel 的时候会读到零值。利用这个特性就可以实现关闭的消息通知。

再看一下其 cancel() 方法的实现:

cancel()方法

cancelCtx 内部跨多个 Goroutine 实现信号传递其实靠的就是一个 done channel;如果要取消这个 Context,那么就需要让所有 <-c.Done() 停止阻塞,这时候最简单的办法就是把这个 channel 直接 close 掉,或者干脆换成一个已经被 close 的 channel

go 复制代码
// cancel closes c.done, cancels each of c's children, and, if
// removeFromParent is true, removes c from its parent's children.
// cancel sets c.cause to cause if this is the first time c is canceled.
func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {
	// context被取消的原因,必传,否则panic
	// 首先判断 err 是不是 nil,如果不是 nil 则直接 panic
    // 这么做的目的是因为 cancel 方法是个私有方法,标准库内任何调用 cancel 的方法保证了一定会传入 err,如果没传那就是非正常调用,所以可以直接 panic
	if err == nil {
		panic("context: internal error: missing cancel error")
	}
	if cause == nil {
		cause = err
	}
	// 对 context 加锁,防止并发更改
	c.mu.Lock()
	// 如果加锁后有并发访问,那么二次判断 err 可以防止重复 cancel 调用
	// 在赋值这个err之前,c.err已经有值了,说明已经被调用过cancel函数了,c这个context已经被取消
	if c.err.Load() != nil {
		c.mu.Unlock()
		return // already canceled
	}

	// 赋值err信息
	// 这里设置了内部的 err,所以上面的判断 c.err != nil 与这里是对应的
	// 也就是说加锁后一定有一个 Goroutine 先 cannel,cannel 后 c.err 一定不为 nil
	c.err.Store(err)
	c.cause = cause

	// 获取通知管道
	d, _ := c.done.Load().(chan struct{})
	// 判断内部的 done channel 是不是为 nil,因为在 context.WithCancel 创建 cancelCtx 的
	// 时候并未立即初始化 done channel(延迟初始化),所以这里可能为 nil
	// 如果 done channel 为 nil,那么就把它设置成共享可重用的一个已经被关闭的 channel
	if d == nil {
		c.done.Store(closedchan)
	} else {
		// 关闭管道
		// 如果 done channel 已经被初始化,则直接 close 它
		close(d)
	}

	// 遍历当前context的所有子节点,调用取消函数
	// 如果当前 Context 下面还有关联的 child Context,且这些 child Context 都是
	// 可以转换成 *cancelCtx 的 Context(见 propagateCancel 方法分析),那么直接遍历 childre map,并且调用 child Context 的 cancel 即可
	// 如果关联的 child Context 不能转换成 *cancelCtx,那么由 propagateCancel 方法中已经创建了单独的 Goroutine 来关闭这些 child Context
	for child := range c.children {
		// NOTE: acquiring the child's lock while holding parent's lock.
		// 递归取消子context
		child.cancel(false, err, cause)
	}

	// 取消动作完成之后,孩子节点置空
	// 清除 c.children map 并解锁
	c.children = nil
	c.mu.Unlock()

	// 如果 removeFromParent 为 true,那么从 parent Context 中清理掉自己
	if removeFromParent {
		// 将自身从父节点children map中移除
		removeChild(c.Context, c)
	}
}

cancel不仅取消当前context,还会遍历当前context的所有子context,递归取消,递归取消完当前context的所有子context后,会将自身从父节点children map中移除,移除函数removeChild源码如下:

go 复制代码
// removeChild removes a context from its parent.
func removeChild(parent Context, child canceler) {
	if s, ok := parent.(stopCtx); ok {
		s.stop()
		return
	}
	p, ok := parentCancelCtx(parent)
	if !ok {
		return
	}
	p.mu.Lock()
	if p.children != nil {
		// 从父context的children中移除
		delete(p.children, child)
	}
	p.mu.Unlock()
}

移除前后效果如下图所示:

在用户层面,创建cancelCtx的方法其实我们之前也接触过,就是withCancel方法,在平常代码中,我们一般用这个方法来派生一个可以用cancel取消函数取消的context,常规用法如下:

go 复制代码
ctx,cancel := context.WithCancel(context.Background())

下面继续跟一下这个WithCancel函数的源码:

WithCancel函数

context.WithCancel(parent Context): 从 parent Context 创建一个带有取消方法的 child Context,该 Context 可以手动调用 cancel

go 复制代码
// WithCancelCause behaves like [WithCancel] but returns a [CancelCauseFunc] instead of a [CancelFunc].
// Calling cancel with a non-nil error (the "cause") records that error in ctx;
// it can then be retrieved using Cause(ctx).
// Calling cancel with nil sets the cause to Canceled.
//
// Example use:
//
//	ctx, cancel := context.WithCancelCause(parent)
//	cancel(myError)
//	ctx.Err() // returns context.Canceled
//	context.Cause(ctx) // returns myError
func WithCancelCause(parent Context) (ctx Context, cancel CancelCauseFunc) {
	c := withCancel(parent)
	// 具体的取消函数cancel的实现
	return c, func(cause error) { c.cancel(true, Canceled, cause) }
}

func withCancel(parent Context) *cancelCtx {
	// 传入的context不能为空,否则将panic
	if parent == nil {
		panic("cannot create context from nil parent")
	}
	// 这里就会创建一个cancelCtx
	c := &cancelCtx{}
	// 这里主要是关联父context ctx和子congtxt c的逻辑
	c.propagateCancel(parent, c)
	return c
}

值得分析的是 propagateCancel(parent, &c) 方法和被其调用的 parentCancelCtx(parent Context) (*cancelCtx, bool) 方法,这两个方法保证了 Context 链可以从顶端到底端的及联 cancel

前面说了调用cancelFunc函数可以级联取消子context,那么为什么可以级联取消呢?propagateCancel函数就是用来做这个工作的,他将父context和子context关联起来,具体的关联逻辑,我们通过源码来分析:

propagateCancel方法

go 复制代码
// propagateCancel arranges for child to be canceled when parent is.
// It sets the parent context of cancelCtx.
func (c *cancelCtx) propagateCancel(parent Context, child canceler) {
	c.Context = parent
	// 获取父context的通信管道 chan struct{}
	// 针对于 context.Background()/TODO() 创建的 Context(emptyCtx),其 done channel 将永远为 nil
	// 对于其他的标准的可取消的 Context(cancelCtx、timerCtx) 调用 Done() 方法将会延迟初始化 done channel(调用时创建)
	// 所以 done channel 为 nil 时说明 parent context 必然永远不会被取消,所以就无需及联到 child Context
	done := parent.Done()
	// done为空,说明父context不会被取消
	if done == nil {
		return // parent is never canceled
	}

	// 如果 done channel 不是 nil,说明 parent Context 是一个可以取消的 Context
	// 这里需要立即判断一下 done channel 是否可读取,如果可以读取说明上面无锁阶段
	// parent Context 已经被取消了,那么应该立即取消 child Context
	// 既然 parent 可能会取消,那就先用一个非阻塞 select 立刻判断 parent 是否已经取消;如果已经取消,就马上取消 child 并返回,避免在"还没建立父子关联"的无锁阶段做无意义的关联/起 goroutine。
	select {
	case <-done:
		// 通信管道收到了消息,说明父context已经被取消,不用重复取消了
		// parent is already canceled
		// 但是父context已经取消,这里子context也应该要取消,由于还没有关联上,所以主动调用cancel取消关联
		child.cancel(false, parent.Err(), Cause(parent))
		return
	default:
	}

	// 从父context中提取出cancelCtx结构
	// parentCancelCtx 用于获取 parent Context 的底层可取消 Context(cancelCtx)
	//
	// 如果 parent Context 本身就是 *cancelCtx 或者是标准库中基于 cancelCtx 衍生的 Context 会返回 true
	// 如果 parent Context 已经取消/或者根本无法取消 会返回 false
	// 如果 parent Context 无法转换为一个 *cancelCtx 也会返回 false
	// 如果 parent Context 是一个自定义深度包装的 cancelCtx(自己定义了 done channel) 则也会返回 false
	if p, ok := parentCancelCtx(parent); ok {
		// parent is a *cancelCtx, or derives from one.
		// 先对 parent Context 加锁,防止更改
		p.mu.Lock()
		// 加锁后双重检查,再次检查父context有没有被取消
		// 因为 ok 为 true 就已经确定了 parent Context 一定为 *cancelCtx,而 cancelCtx 取消时必然设置 err
		// 所以并发加锁情况下如果 parent Context 的 err 不为空说明已经被取消了
		if err := p.err.Load(); err != nil {
			// parent has already been canceled
			// parent Context 已经被取消,则直接及联取消 child Context
			child.cancel(false, err.(error), p.cause)
		} else {
			// 父context没有被取消
			// 在 ok 为 true 时确定了 parent Context 一定为 *cancelCtx,此时 err 为 nil
			// 这说明 parent Context 还没被取消,这时候要在 parent Context 的 children map 中关联 child Context
			// 这个 children map 在 parent Context 被取消时会被遍历然后批量调用 child Context 的取消方法
			if p.children == nil {
				// 确保 p.children 这个 map 存在,然后把当前 child 记录进去,用于"父取消时能把所有子也取消"
				// 创建父context的children map
				// nil map 不能写入,对 nil map 赋值会 panic
				// 所以第一次要往里塞 child 之前,必须先初始化
				// 为什么不在创建 cancelCtx 时就 make 好?
				// 因为并不是每个 cancelCtx 都会有子节点。很多 ctx 没有 child,提前创建 map 就浪费内存。
				// 所以这里采用 懒初始化(lazy init):第一次需要用的时候再 make
				p.children = make(map[canceler]struct{})
			}
			// 把当前子context加入到children map里面
			p.children[child] = struct{}{}
		}
		p.mu.Unlock()
		return
	}

	if a, ok := parent.(afterFuncer); ok {
		// parent implements an AfterFunc method.
		c.mu.Lock()
		stop := a.AfterFunc(func() {
			child.cancel(false, parent.Err(), Cause(parent))
		})
		c.Context = stopCtx{
			Context: parent,
			stop:    stop,
		}
		c.mu.Unlock()
		return
	}

	// ok 为 false,说明: "parent Context 已经取消" 或 "根本无法取消" 或 "无法转换为一个 *cancelCtx" 或 "是一个自定义深度包装的 cancelCtx"
	// 从父context中没有提取出cancelCtx结构
	goroutines.Add(1)
	// 新起一个goroutine监控父子context的通信管道有没有取消信号
	// 由于代码在方法开始时就判断了 parent Context "已经取消"、"根本无法取消" 这两种情况
	// 所以这两种情况在这里不会发生,因此 <-parent.Done() 不会产生 panic
	// 
	// 唯一剩下的可能就是 parent Context "无法转换为一个 *cancelCtx" 或 "是一个被覆盖了 done channel 的自定义 cancelCtx"
	// 这种两种情况下无法通过 parent Context 的 children map 建立关联,只能通过创建一个 Goroutine 来完成及联取消的操作
	go func() {
		select {
		case <-parent.Done():
			child.cancel(false, parent.Err(), Cause(parent))
		case <-child.Done():
		}
	}()
}

canceler 是 context 包内部为了取消传播而抽象出来的私有接口。propagateCancel 既需要在父 ctx 取消时调用 child.cancel(...),又需要在某些情况下监听 child.Done() 来避免 goroutine 泄漏,因此 canceler 必须包含 cancel() 和 Done()。cancelCtx/timerCtx 的 Done() 方法签名与 Context.Done() 相同,所以同一个 Done() 同时满足 Context 与 canceler 两个接口。

看一下这个提取父context的cancelCtx结构的parentCancelCtx方法:

parentCancelCtx方法

go 复制代码
// parentCancelCtx returns the underlying *cancelCtx for parent.
// It does this by looking up parent.Value(&cancelCtxKey) to find
// the innermost enclosing *cancelCtx and then checking whether
// parent.Done() matches that *cancelCtx. (If not, the *cancelCtx
// has been wrapped in a custom implementation providing a
// different done channel, in which case we should not bypass it.)
// parentCancelCtx 负责从 parent Context 中取出底层的 cancelCtx
func parentCancelCtx(parent Context) (*cancelCtx, bool) {
	// 如果 parent context 的 done 为 nil 说明不支持 cancel,那么就不可能是 cancelCtx
	// 如果 parent context 的 done 为 可复用的 closedchan 说明 parent context 已经 cancel 了
	// 此时取出 cancelCtx 没有意义
	done := parent.Done()

	// 从父context的取消信息管道为空,说明父context不会被取消
	// closedchan is a reusable closed channel.
	// var closedchan = make(chan struct{})
	// done == closedchan,表明ctx不是标准的 cancelCtx,可能是自定义的结构实现了 context.Context 接口
	if done == closedchan || done == nil {
		return nil, false
	}

	// 通过context的value方法从父context中提取出cancelCtx
	// 如果 parent context 属于原生的 *cancelCtx 或衍生类型(timerCtx) 需要继续进行后续判断
	// 如果 parent context 无法转换到 *cancelCtx,则认为非 cancelCtx,返回 nil,fasle
	p, ok := parent.Value(&cancelCtxKey).(*cancelCtx)
	if !ok {
		return nil, false
	}
	// 判断父context里的通信管道和cancelCtx里的管道是否一致
	// 经过上面的判断后,说明 parent context 可以被转换为 *cancelCtx,这时存在多种情况:
	//   - parent context 就是 *cancelCtx
	//   - parent context 是标准库中的 timerCtx
	//   - parent context 是个自己自定义包装的 cancelCtx
	//
	// 针对这 3 种情况需要进行判断,判断方法就是: 
	//   判断 parent context 通过 Done() 方法获取的 done channel 与 Value 查找到的 context 的 done channel 是否一致
	// 
	// 一致情况说明 parent context 为 cancelCtx 或 timerCtx 或 自定义的 cancelCtx 且未重写 Done(),
	// 这种情况下可以认为拿到了底层的 *cancelCtx
	// 
	// 不一致情况说明 parent context 是一个自定义的 cancelCtx 且重写了 Done() 方法,并且并未返回标准 *cancelCtx 的 done channel,这种情况需要单独处理,故返回 nil, false
	pdone, _ := p.done.Load().(chan struct{})

	// 不一致,表明parent不是标准的cancelCtx
	if pdone != done {
		return nil, false
	}
	// 返回cancelCtx
	return p, true
}

总结一下通过WithCancel函数在派生可取消的子context的过程中,通过propagateCancel函数关联父子context可能遇到的几种情形:

  1. 父context的通信管道done为空或者已经被取消,就不用关联了,直接取消当前子context即可

  2. 父context可以被取消,但是还未被取消,并且父context可以提取出标准的cancelCtx结构,则创建父context的children map,将当前子context加入到这个map中

  3. 父context可以被取消,但是还未被取消,父context不能提取出标准的cancelCtx结构,新起一个goroutine监控父子context的通信管道有没有取消信号



timerCtx

timerCtx 实际上是在 cancelCtx 之上构建的,唯一的区别就是增加了计时器和截止时间。不仅拥有像cancelCtx一样,可以通过调用取消函数cancelFun来取消子context的方式,还可以设置一个截止时间deadline,在deadline到来的时,自动取消context。

有了这两个配置以后就可以在特定时间进行自动取消,WithDeadline(parent Context, d time.Time) 和 WithTimeout(parent Context, timeout time.Duration) 方法返回的都是这个 timerCtx。

首先看一下timerCtx的结构定义:

go 复制代码
// 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
}
go 复制代码
// 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()
}

看到它内置了cancelCtx,所以cancelCtx拥有的方法,他可以调用cancelCtx的方法,能够主动取消context,再看一下timerCtx自身的cancel方法实现:

go 复制代码
func (c *timerCtx) cancel(removeFromParent bool, err, cause error) {
	// 直接调用cancelCtx的cancel方
	c.cancelCtx.cancel(false, err, cause)
	if removeFromParent {
		// Remove this timerCtx from its parent cancelCtx's children.
		// 将当前子context从父context中删除
		removeChild(c.cancelCtx.Context, c)
	}
	c.mu.Lock()
	// 要关闭掉定时器,因为手动取消过一次了,如果不关闭,在deadline 到来时,不会再次取消,造成错误
	if c.timer != nil {
		c.timer.Stop()
		c.timer = nil
	}
	c.mu.Unlock()
}

同样在用户层面,我们一般通过WithTimeout或者WithDeadline来创建一个timerCtx

go 复制代码
ctx, cancel := context.WithDeadline(context.Background(),time.Now().Add(4*time.Second)) // 截止时间当前时间4s后​ 
ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second) // 超时时间为4s后

context.WithTimeout(parent Context, timeout time.Duration): 与 WithDeadline 类似,只不过指定的是一个从当前时间开始的超时时间

在WithTimeout内部其实也是调用了WithDeadline,所以只用分析WithDeadline方法即可:

WithDeadline 函数

context.WithDeadline(parent Context, d time.Time): 从 parent Context 创建一个带有取消方法的 child Context,不同的是当到达 d 时间后该 Context 将自动取消

go 复制代码
// WithDeadline returns a derived context that points to the parent context
// but has the deadline adjusted to be no later than d. If the parent's
// deadline is already earlier than d, WithDeadline(parent, d) is semantically
// equivalent to parent. The returned [Context.Done] channel is closed when
// the deadline expires, 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 WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
	return WithDeadlineCause(parent, d, nil)
}

// WithDeadlineCause behaves like [WithDeadline] but also sets the cause of the
// returned Context when the deadline is exceeded. The returned [CancelFunc] does
// not set the cause.
func WithDeadlineCause(parent Context, d time.Time, cause error) (Context, CancelFunc) {
	// 父context为空,直接panic
	// 与 cancelCtx 一样先检查一下 parent Context
	if parent == nil {
		panic("cannot create context from nil parent")
	}

	// 如果父context的deadline早于这里要设置的子context的截止时间
	// 判断 parent Context 是否支持 Deadline,如果支持的话需要判断 parent Context 的截止时间
    // 假设 parent Context 的截止时间早于当前设置的截止时间,那就意味着 parent Context 肯定会先被 cancel,同样由于 parent Context 的 cancel 会导致当前这个 child Context 也会被 cancel
    // 所以这时候直接返回一个 cancelCtx 就行了,计时器已经没有必要存在了
	if cur, ok := parent.Deadline(); ok && cur.Before(d) {
		// The current deadline is already sooner than the new one.
		// 直接取消父context即可,不需要再管子context的取消时间,直接构建一个可以取消的子context​ 
		// 因为父context的到期时间早于子context,当父context被取消的时候,这个子context肯定会被级联取消
		return WithCancel(parent)
	}
	// 创建timerCtx对象
	c := &timerCtx{
		deadline: d,
	}
	// 关联父context
	// 与 cancelCtx 一样的传播操作
	c.cancelCtx.propagateCancel(parent, c)
	
	// 获取距离设置的子context过期时间的时间差
	// 判断当前时间是否已经过了截止日期,如果超过了直接 cancel
	dur := time.Until(d)
	// 时间差小于0,表示已经过期了,直接取消
	if dur <= 0 {
		c.cancel(true, DeadlineExceeded, cause) // deadline has already passed
		return c, func() { c.cancel(false, Canceled, nil) }
	}
	// 所有 check 都没问题的情况下,创建一个定时器,在到时间后自动 cancel
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.err.Load() == nil {
		// 根据时间差,创建一个定时器,到deadline的时候定时触发取消
		c.timer = time.AfterFunc(dur, func() {
			c.cancel(true, DeadlineExceeded, cause)
		})
	}
	return c, func() { c.cancel(true, Canceled, nil) }
}

c 虽然刚创建,但很快就"暴露"给父 context 的取消链了

go 复制代码
c.cancelCtx.propagateCancel(parent, c)

propagateCancel 会做两件可能导致 c 立即被 cancel 的事:

  • 情况 A:父 context 已经取消
  • 情况 B:父 context 未来取消,但取消发生得非常快

所以判断 c.err.Load() == nil 是为了避免"已取消的 ctx 还去建 timer"

所以,父context未取消的情况下,在创建timerCtx的时候有两种情况:

  1. 设置的截止时间晚于父context的截止时间,则不会创建timerCtx,会直接创建一个可取消的context,因为父context的截止时间更早,会被取消,父context被取消的时候会级联取消这个子context
  2. 设置的截止时间早于父context的截止时间,会创建一个正常的timerCtx

cancel方法

了解了 cancelCtx 的取消流程以后再来看 timerCtx 的取消就相对简单的多,主要就是调用一下里面的 cancelCtx 的 cancel,然后再把定时器停掉:

go 复制代码
func (c *timerCtx) cancel(removeFromParent bool, err, cause error) {
	c.cancelCtx.cancel(false, err, cause)
	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()
}

valueCtx

相对于 cancelCtx 还有 timerCtx,valueCtx 实在是过于简单,因为它没有及联的取消逻辑,也没有过于复杂的 kv 存储

valueCtx的作用与上述三个context有点不同,他不是用于父子context之间的取消的,而是用于数据共享。作用类似于一个map,不过数据的存储和读取是在两个context,用于goroutine之间的数据传递。

valueCtx 内部同样包含了一个 Context 接口实例,目的也是可以作为 child Context,同时为了保证其 "Value" 特性,其内部包含了两个无限制变量 key, val interface{};在调用 valueCtx.Value(key interface{}) 会进行递归向上查找,但是这个查找只负责查找 "直系" Context,也就是说可以无限递归查找 parent Context 是否包含这个 key,但是无法查找兄弟 Context 是否包含。

valueCtx的结构定义如下:

go 复制代码
// 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
}

WithValue 不加锁没问题,因为它只是创建并返回一个不可变的 valueCtx。并发问题只会出现在:你塞进去的 val 是可变共享对象,并且被多个 goroutine 同时读写。valueCtx 本身不需要锁,因为它是不可变、只读的。需要注意的并发风险在于:你存到 Context 里的 val 若是可变共享对象,你自己要保证它的并发安全。

valueCtx内置了Context,所以他也是一个context接口的实现,但是其没有实现canceler接口,所以他不能作为context的取消,valueCtx实现了String()方法和Value方法,String()比较简单,就不细看了,下面看一下Value方法

go 复制代码
// WithValue returns a copy of parent in which the value associated with key is
// val.
//
// Use context Values only for request-scoped data that transits processes and
// APIs, not for passing optional parameters to functions.
//
// The provided key must be comparable and should not be of type
// string or any other built-in type to avoid collisions between
// packages using context. Users of WithValue should define their own
// types for keys. To avoid allocating when assigning to an
// interface{}, context keys often have concrete type
// struct{}. Alternatively, exported context key variables' static
// type should be a pointer or interface.
// WithValue 方法负责创建 valueCtx
func WithValue(parent Context, key, val interface{}) Context {
    // parent 检测
	if parent == nil {
		panic("cannot create context from nil parent")
	}
    // key 检测
	if key == nil {
		panic("nil key")
	}
    // key 必须是可比较的
	if !reflectlite.TypeOf(key).Comparable() {
		panic("key is not comparable")
	}
	return &valueCtx{parent, key, val}
}

// 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{} {
    // 先判断当前 Context 里有没有这个 key
	if c.key == key {
		return c.val
	}
    // 如果没有递归向上查找
	return c.Context.Value(key)
}

Value 方法

go 复制代码
func (c *valueCtx) Value(key any) any {
	if c.key == key {
		return c.val
	}
	return value(c.Context, key)
}

方法很简单,就是向上递归的查找key所对应的value,如果找到则直接返回value,否则查找该context的父context,一直顺着context向上,最终找到根节点(一般是 emptyCtx),直接返回一个nil。查找过程如下图:

从定义可以看出valueCtx中存储着一对键值对,具体是怎么用的呢?同样我们一般使用WithValue方法派生出一个valueCtx

go 复制代码
ctx := context.WithValue(context.Background(), "key1", "value1")

WithValue函数源码如下:

context.WithValue(parent Context, key, val interface{}): 从 parent Context 创建一个 child Context,该 Context 可以存储一个键值对,同时这是一个不可取消的 Context

go 复制代码
// WithValue returns a derived context that points to the parent Context.
// In the derived context, the value associated with key is val.
//
// Use context Values only for request-scoped data that transits processes and
// APIs, not for passing optional parameters to functions.
//
// The provided key must be comparable and should not be of type
// string or any other built-in type to avoid collisions between
// packages using context. Users of WithValue should define their own
// types for keys. To avoid allocating when assigning to an
// interface{}, context keys often have concrete type
// struct{}. Alternatively, exported context key variables' static
// type should be a pointer or interface.
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}
}

withValue的方法实现很简单,就是创建一个valueCtx,将key和value设置到valueCtx返回。

context面试与分析

1、context 结构是什么样的?

go语言里的context实际上是一个接口,提供了四种方法:

go 复制代码
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}

2、context 使用场景和用途?(基本必问)

  1. context 主要用来在 goroutine 之间传递上下文信息,比如传递请求的trace_id,以便于追踪全局唯一请求

  2. 另一个用处是可以用来做取消控制,通过取消信号和超时时间来控制子goroutine的退出,防止goroutine泄漏

包括:取消信号,超时时间,截止时间,k-v 等。

3、context 有哪几种数据结构的实现

有emptyCtx、cancelCtx、timerCtx、valueCtx四种实现

emptyCtx:emptyCtx虽然实现了context接口,但是不具备任何功能,因为实现很简单,基本都是直接返回空值

cancelCtx:cancelCtx同时实现Context和Canceler接口,通过取消函数cancelFunc实现退出通知。注意其退出通知机制不但通知自己,同时也通知其children节点。

timerCtx:timerCtx是一个实现了Context接口的具体类型,其内部封装了cancelCtx类型实例,同时也有deadline变量,用来实现定时退出通知

valueCtx:valueCtx是一个实现了Context接口的具体类型,其内部封装了Context接口类型,同时也封装了一个k/v的存储变量,其是一个实现了数据传递

之后我会持续更新,如果喜欢我的文章,请记得一键三连哦,点赞关注收藏,你的每一个赞每一份关注每一次收藏都将是我前进路上的无限动力 !!!↖(▔▽▔)↗感谢支持!

相关推荐
ShineWinsu3 小时前
对于C++:类和对象的解析—下(第二部分)
c++·面试·笔试·对象··工作·stati
码农水水3 小时前
国家电网Java面试被问:TCP的BBR拥塞控制算法原理
java·开发语言·网络·分布式·面试·wpf
2013092416273 小时前
1968年 Hart, Nilsson, Raphael 《最小成本路径启发式确定的形式基础》A* 算法深度研究报告
人工智能·算法
如何原谅奋力过但无声3 小时前
【力扣-Python-滑动窗口经典题】567.字符串的排列 | 424.替换后的最长重复字符 | 76.最小覆盖子串
算法·leetcode
浮尘笔记4 小时前
Go语言临时对象池:sync.Pool的原理与使用
开发语言·后端·golang
咕噜咕噜啦啦4 小时前
Java期末习题速通
java·开发语言
BHXDML4 小时前
第七章:类与对象(c++)
开发语言·c++
玄冥剑尊4 小时前
贪心算法进阶
算法·贪心算法
玄冥剑尊4 小时前
贪心算法深化 I
算法·贪心算法
52Hz1185 小时前
力扣73.矩阵置零、54.螺旋矩阵、48.旋转图像
python·算法·leetcode·矩阵