文章目录
- context是什么
- context的底层实现
- 接口说明
- context实现
- context面试与分析
- [1、context 结构是什么样的?](#1、context 结构是什么样的?)
- [2、context 使用场景和用途?(基本必问)](#2、context 使用场景和用途?(基本必问))
- [3、context 有哪几种数据结构的实现](#3、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() 关闭
更准确的关系是:
- 上游(创建者/控制者)调用 cancel()(或超时到期、deadline 到期)
- 该 context 的 Done() 返回的 channel 会被 close
- 下游 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可能遇到的几种情形:
-
父context的通信管道done为空或者已经被取消,就不用关联了,直接取消当前子context即可
-
父context可以被取消,但是还未被取消,并且父context可以提取出标准的cancelCtx结构,则创建父context的children map,将当前子context加入到这个map中
-
父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的时候有两种情况:
- 设置的截止时间晚于父context的截止时间,则不会创建timerCtx,会直接创建一个可取消的context,因为父context的截止时间更早,会被取消,父context被取消的时候会级联取消这个子context
- 设置的截止时间早于父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 使用场景和用途?(基本必问)
-
context 主要用来在 goroutine 之间传递上下文信息,比如传递请求的trace_id,以便于追踪全局唯一请求
-
另一个用处是可以用来做取消控制,通过取消信号和超时时间来控制子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的存储变量,其是一个实现了数据传递
之后我会持续更新,如果喜欢我的文章,请记得一键三连哦,点赞关注收藏,你的每一个赞每一份关注每一次收藏都将是我前进路上的无限动力 !!!↖(▔▽▔)↗感谢支持!