context
基于go1.23.10
功能介绍
context
包是Go语言中用于管理请求范围数据、取消信号和超时的重要工具。它特别适用于跨API边界和多个goroutine之间传递请求相关的值、取消信号和截止时间。
context的实现的功能本质都可以使用for+select+chan实现:
比如取消的功能,本质是就是使用select+chan通知,然后return即可;
type MyContext struct {
cancelChan chan struct{}
}
func NewMyContext() *MyContext {
return &MyContext{cancelChan: make(chan struct{})}
}
func (c *MyContext) Cancel() {
close(c.cancelChan) // 一旦关闭一个 channel,所有读取操作(<-chan)会立即成功,返回通道类型的“零值”。
}
func (c *MyContext) Done() <-chan struct{} {
return c.cancelChan
}
ctx := NewMyContext()
go func() {
for {
select {
case <-ctx.Done(): // 这里调用自己的管道,不取消的时候阻塞着,调用Cancel时候,管道关闭,发送通知,就取消了当前goroutine
fmt.Println("任务被取消")
return
default:
fmt.Println("工作中...")
time.Sleep(500 * time.Millisecond)
}
}
}()
time.Sleep(2 * time.Second)
ctx.Cancel()
设计总览
Context包的结构图如下:
exported
指的是“导出的标识符”,也就是那些首字母大写的函数、类型、方法、变量或常量。exported
函数就是可以在包外使用的函数。
Context是一个接口,定义了四个方法:
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
有四个exported函数供使用
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithValue(parent Context, key, val interface{}) Context
cancelCtx
// 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
cause error // set to non-nil by the first cancel call
}
cancelCtx这个结构体,Context直接是内嵌在里面的。因此可以把 context 看作一条 带着格式样标记的链表。
Go 的 context
包,其实就是一条单向链表(链式结构),每次调用 WithCancel
、WithTimeout
、WithValue
,都会生成一个新的 ctx
节点,把 parent
链接上去。
每个 ctx 都持有一个父节点(一般字段是
Context
或者parent
)。
查找 Value / Done / Deadline 时,都是从当前 ctx 往父节点一级级递归/循环查找。
最终会走到根结点backgroundCtx
或todoCtx
,这两个返回空值,查找结束。
内嵌
装饰器模式的一种实现,内嵌接口,然后传入接口的实现对象,有点像Java中的继承与重写(不过还是有些理念的区别,就好像蝙蝠和鸟都会飞一样),下面的例子可以体现。type Person interface { Age() Name() } type GG struct { } func (g GG) Age() { fmt.Println("gg实现了这个方法") } func (g GG) Name() { fmt.Println("gg写了name") } type MM struct { Person } func (c MM) Name() { fmt.Println("mm重写了name") } func TestName(t *testing.T) { mm := MM{Person: GG{}} mm.Age() // gg实现了这个方法 mm.Name()// mm重写了name }
cancelCtx除了实现Context这个接口,还实现了canceler这个接口:
// 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{}
}
cancelCtx这个结构体,是exported函数WithCancel的基石
WithCancel
主要核心的方法是propagateCancel
propagateCancel
// WithCancel returns a copy of parent with a new Done channel. The returned
// context's Done channel is closed 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 WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
c := withCancel(parent)
return c, func() { c.cancel(true, Canceled, nil) }
}
func withCancel(parent Context) *cancelCtx {
if parent == nil {
panic("cannot create context from nil parent")
}
c := &cancelCtx{}
c.propagateCancel(parent, c) // 建立父子级ctx之间的通信联系,有很多case需要考虑,不是单纯的把子ctx加到父ctx的children中即可
return c
}
// propagateCancel 用来建立父子 Context 的取消传播关系。
// 当 parent 被取消时,child 也会被取消。
// 它会把 cancelCtx 的父 context 设置为 parent。
func (c *cancelCtx) propagateCancel(parent Context, child canceler) {
// 设置当前 cancelCtx 的父 Context
c.Context = parent
// 获取 parent 的 Done channel,用于监听是否被取消
done := parent.Done()
if done == nil {
// 如果 parent 永远不会被取消(比如 Background() 或 TODO()),则直接返回
// parent不需要children,所以child不用找parent
return
}
// 非阻塞检查:如果 parent 已经被取消
select {
case <-done:
// parent 已经被取消,立即取消 child
child.cancel(false, parent.Err(), Cause(parent))
return
default:
// parent 还没被取消,继续后续逻辑
}
// 尝试将父context转换为*cancelCtx类型(或它的派生类型)
// 是不是有点奇怪传进来的ctx不就是这个类型吗?为什么还要再检查呢?非也,propagateCancel方法
// 在很多处都有使用,传进来的parent不一定都是*cancelCtx类型
if p, ok := parentCancelCtx(parent); ok {
// 如果 parent 是 *cancelCtx 或者基于 *cancelCtx 派生的
p.mu.Lock()
if p.err != nil {
// 如果 parent 已经被取消,则立刻取消 child
child.cancel(false, p.err, p.cause)
} else {
// 否则,parent 还没被取消
// 把 child 注册到 parent 的 children 集合里
// 这样当 parent 被取消时,会遍历 children 并取消
if p.children == nil {
p.children = make(map[canceler]struct{})
}
p.children[child] = struct{}{}
}
p.mu.Unlock()
return
}
// NOTE: 父 Context 实现了 afterFuncer 接口,扩展功能,context包没把这个接口实现,可以自己扩展实现
// 测试文件 afterfunc_test.go 中 *afterFuncCtx 实现了 afterFuncer 接口,是一个很好的样例
if a, ok := parent.(afterFuncer); ok {
c.mu.Lock()
stop := a.AfterFunc(func() {// 注册子 Context 取消功能到父 Context,当父 Context 取消时,能级联取消子 Context
child.cancel(false, parent.Err(), Cause(parent))
})
// 用 stopCtx 包装 parent,便于后续调用 stop() 移除回调
c.Context = stopCtx{// 将当前 *cancelCtx 的直接父 Context 设置为 stopCtx
Context: parent, // stopCtx 的父 Context 设置为 parent
stop: stop,
}
c.mu.Unlock()
return
}
// 如果 parent 既不是 *cancelCtx,也没实现 afterFuncer
// 那就只能启一个 goroutine 去监听 parent.Done()
goroutines.Add(1)
go func() {
select {
case <-parent.Done():
// parent 被取消时,取消 child
child.cancel(false, parent.Err(), Cause(parent))
case <-child.Done():
// child 自己先被取消了,就直接退出 goroutine
}
}()
}
propagateCancel
是一个很关键的方法,主要是用于建立parent与child的联系,在func withCancel(parent Context) *cancelCtx
可以知道,parent就是传入的ctx,而child是新声明的ctx,是为了把这个新的ctx和传入的ctx建立联系。
详细说明:
propagateCancel() 方法将 cancelCtx 对象向上传播挂载到父 context 的 children 属性集合中,这样当父 context 被取消时,子 context 也会被级联取消。这个方法逻辑稍微有点多,也是 context 包中最复杂的方法了,拿下它,后面的代码就都很简单了。
首先将 parent 参数记录到 cancelCtx.Context 属性中,作为父 context。接下来会对父 context 做各种判断,以此来决定如何处理子 context。
第 9 行通过 parent.Done() 拿到父 context 的 done channel,如果值为 nil,则说明父 context 没有取消功能,所以不必传播子 context 的取消功能到父 context。
第 17 行使用 select…case… 来监听 <-done 是否被关闭,如果已关闭,则说明父 context 已经被取消,那么直接调用 child.cancel() 取消子 context。因为 context 的取消功能是从上到下级联取消,所以父 context 被取消,那么子 context 也一定要取消。
如果父 context 尚未取消,则在第 29 行判断父 context 是否为 *cancelCtx 或者从 *cancelCtx 派生而来。如果是,则判断父 context 的 err 属性是否有值,有值则说明父 context 已经被取消,那么直接取消子 context;否则将子 context 加入到这个 *cancelCtx 类型的父 context 的 children 属性集合中。
如果父 context 不是 *cancelCtx 类型,在第 50 行判断父 context 是否实现了 afterFuncer 接口。如果实现了,则新建一个 stopCtx 作为当前 *cancelCtx 的父 context。
最终,如果之前对父 context 的判断都不成立,则开启一个新的 goroutine 来监听父 context 和子 context 的取消信号。如果父 context 被取消,则级联取消子 context;如果子 context 被取消,则直接退出 goroutine。
至此 propagateCancel() 方法的主要逻辑就梳理完了。
以上的详细说明引自 Go 并发控制:context 源码解读 - 知乎
cancel
propagateCancel
中提及了cancel()
这个方法,这也是很核心的一个方法,cancelCtx
的实现如下:
// 这个函数用于取消当前context,并递归取消所有子context,具体操作和目的如下:
// 1. 关闭 c.done channel,表示当前 context 已被取消。
// 2. 递归取消所有子 context。
// 3. 如果 removeFromParent = true,则把自己从父 context 的 children 中移除。
// 4. 如果是第一次取消,则记录取消的原因(err/cause)。
func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {
// err 不能为空,取消必须有原因(通常是 context.Canceled 或 context.DeadlineExceeded)
if err == nil {
panic("context: internal error: missing cancel error")
}
// 如果调用方没有传 cause,就默认用 err 作为 cause
if cause == nil {
cause = err
}
c.mu.Lock()
// 如果 c.err 已经有值,说明当前 context 已经被取消过了
if c.err != nil {
c.mu.Unlock()
return // 避免重复取消
}
// 设置取消原因
c.err = err
c.cause = cause
// 获取 done channel
d, _ := c.done.Load().(chan struct{})
if d == nil {
// 如果还没初始化,直接存入全局的 closedchan(已关闭的 channel 常量)
// 这样后续所有 <-c.Done() 都会立即返回
c.done.Store(closedchan)
// 这里对应的情况是,还没有调用ctx.Done()的时候,就已经调用cancel()方法了
// 为了保持语义一致性:只要调用了cancel()所有的channel都会关闭,哪怕现在没开后续会开的
// 有了这个设计,无论何时调用 ctx.Done(),都会立即返回并 unblock。
} else {
// 否则关闭已存在的 channel,通知所有等待 <-c.Done() 的 goroutine
close(d)
}
// 遍历所有子 context,递归取消
// 这里也就是 propagateCancel 方法建立联系的用处
for child := range c.children {
// 注意:这里在持有 parent 的锁的情况下调用 child.cancel
// 这是 Go context 源码中的一个经典点,允许这种锁的嵌套调用
child.cancel(false, err, cause)
}
// 清空 children map,释放引用,避免内存泄漏
c.children = nil
c.mu.Unlock()
// 如果需要从父节点移除自己
if removeFromParent {
removeChild(c.Context, c)
}
}
上面讲到传入一个已经关闭的channel,这个设计和Done()
方法是连在一起的:
func (c *cancelCtx) Done() <-chan struct{} {
d := c.done.Load()
if d != nil {// 这里发现已经存在channel了,就直接返回,如果是上面给到的closeedchan,那么在select中会直接命中
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{})
}
parentCancelCtx
propagateCancel
方法下面提到了parentCancelCtx
方法,主要是用于判断是不是cancelCtx的实例,这是一个用的很多的内部方法:
// 这个函数用于从父context(即parent)中提取底层的*cancelCtx实例
// 具体:
// 1. 先通过 parent.Value(&cancelCtxKey) 找到离 parent 最近的 *cancelCtx;
// 2. 再检查 parent.Done() 是否和这个 *cancelCtx 的 done channel 一致。
// 如果一致,说明 parent 没被包装,可以直接操作 parent 的 *cancelCtx。
// 如果不一致,说明 parent 被包装过(例如自定义了 Done()),不能绕过包装层。
func parentCancelCtx(parent Context) (*cancelCtx, bool) {
// 拿到 parent 的 Done channel
done := parent.Done()
// 如果 Done() 是全局 closedchan(永远已关闭)或 nil(永远不会关闭)
// 说明 parent 不会真正传播取消,不用继续
if done == closedchan || done == nil {
return nil, false
}
// 从 parent 的 Value() 中取出 *cancelCtx
// cancelCtx实现Value方法时,把cancelCtxKey写死在方法里了,即如果传入的
// 是cancelCtxKey的指针(cancelCtxKey是包变量int类型),就将cancelCtx的
// 实现本身返回,断言一下就知道这个是不是cancelCtx类型了
p, ok := parent.Value(&cancelCtxKey).(*cancelCtx)
if !ok {
// parent 根本不是 *cancelCtx,直接返回失败
return nil, false
}
// 拿到这个 *cancelCtx 内部的 done channel
pdone, _ := p.done.Load().(chan struct{})
// 核心检查:如果 parent.Done() 返回的 channel != *cancelCtx.done
// 说明 parent 被包装过(比如自定义了一个不同的 Done channel)
// 这种情况下我们不能“越过包装”直接操作内部 cancelCtx
if pdone != done {
return nil, false
}
// 走到这里说明 parent 确实是 *cancelCtx,且没被包装,可以直接用
return p, true
}
cancelCtx的Value
cancelCtx
实现的Value如下:
// cancelCtx实现的Value配合parentCancelCtx中的判断,可以快速判断一个ctx是不是cancelCtx类型
func (c *cancelCtx) Value(key any) any {
if key == &cancelCtxKey { // 这里可以看到,如果key是cancelCtxKey,就可以直接返回c
return c
}
return value(c.Context, key)
}
// 相对的,valueCtx是没有这个逻辑的,这样就可以有区分了。
func (c *valueCtx) Value(key any) any {
if c.key == key {
return c.val
}
return value(c.Context, key)
}
func value(c Context, key any) any {
for { // 这是一个for,用于c的类型转化后去到对应的case
switch ctx := c.(type) {
case *valueCtx:
if key == ctx.key {
return ctx.val
}
c = ctx.Context
case *cancelCtx:
if key == &cancelCtxKey {
return c
}
c = ctx.Context
case withoutCancelCtx:
if key == &cancelCtxKey {
// This implements Cause(ctx) == nil
// when ctx is created using WithoutCancel.
return nil
}
c = ctx.c
case *timerCtx:
if key == &cancelCtxKey {
return &ctx.cancelCtx
}
c = ctx.Context
case backgroundCtx, todoCtx:
return nil
default:
return c.Value(key)
}
}
}
WithCancelCause
自定义err,和WithCancel
基本一致,如果不用这个方法,返回的是系统默认err,就是context canceled。如果想自定义, 可以传一个,用context.Cause()
获取自定义err即可。
// WithCancelCause 的作用:
// 类似于 WithCancel,但返回的是 CancelCauseFunc 而不是 CancelFunc。
// CancelCauseFunc 允许调用方传入一个 error(即 "cause"),用来记录取消的具体原因。
// 之后可以通过 context.Cause(ctx) 取回这个 error。
//
// 语义:
// - 调用 cancel(causeError) 时:
// 1. ctx.Err() 依旧返回 context.Canceled(保持向后兼容);
// 2. 但 context.Cause(ctx) 返回传入的 causeError。
// - 如果 cancel(nil),则会把 cause 设置为 context.Canceled。
//
// 使用示例:
// ctx, cancel := context.WithCancelCause(parent)
// cancel(myError)
// ctx.Err() // == context.Canceled
// context.Cause(ctx) // == myError
func WithCancelCause(parent Context) (ctx Context, cancel CancelCauseFunc) {
// 实际内部还是调用 withCancel(parent),返回一个 *cancelCtx
c := withCancel(parent)
// 返回值:
// 1. ctx:就是这个 *cancelCtx
// 2. cancel:一个闭包,接收一个 error 作为 cause
// 调用时执行 c.cancel(true, Canceled, cause)
// - 第一个参数 true 表示需要从父 context 的 children 集合中移除
// - 第二个参数固定传 context.Canceled(Err() 的表现始终一致)
// - 第三个参数就是调用方传入的 cause,会记录到 cancelCtx.cause 字段
return c, func(cause error) { c.cancel(true, Canceled, cause) }
}
// 默认是这个err,context包固定的一个error
// Canceled is the error returned by [Context.Err] when the context is canceled.
var Canceled = errors.New("context canceled")
这是一个简单的测例:
func TestName5(t *testing.T) {
bg := context.Background()
//ctx, cancelFunc := context.WithCancel(bg)
ctx, cancelFunc := context.WithCancelCause(bg)
cancelFunc(errors.New("my error"))
err := context.Cause(ctx)
if err != nil {
fmt.Println(err.Error())
}
err = ctx.Err()
if err != nil {
fmt.Println(err.Error())
return
}
}
timerCtx
基于cancelCtx的一个结构体,实现了Done and Err,可以通过停止timer来实现定时取消的功能。
// 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
}
对此有四个方法是基于此结构体的:WithDeadline(), WithDeadlineCause(), WithTimeout(),WithTimeoutCause()
WithDeadlineCause
是最底层的方法,另外三个都是对这个方法的封装,
WithTimeout (相对时间)
→ WithDeadline (绝对时间,无原因)
→ WithDeadlineCause (绝对时间,带原因)
从简单向复杂去讲,其中WithTimeout()
使用起来最简单:
WithTimeout
这个函数是可以实现定时取消,不用手动调用返回的函数,一个例子:
bg := context.Background()
timeout, c := context.WithTimeout(bg, 5*time.Second)
defer c() // 如果直接调用c(),那么会直接取消
for {
select {
case <-timeout.Done(): // 会在5*time.Second后发送取消通知
fmt.Println("end time")
time.Sleep(time.Second)
return
default:
time.Sleep(time.Second)
fmt.Println("click")
}
}
来看一下源码:
// WithTimeout returns WithDeadline(parent, time.Now().Add(timeout)).
//
// Canceling this context releases resources associated with it, so code should
// call cancel as soon as the operations running in this [Context] complete:
//
// func slowOperationWithTimeout(ctx context.Context) (Result, error) {
// ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
// defer cancel() // releases resources if slowOperation completes before timeout elapses
// return slowOperation(ctx)
// }
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout)) // 可以看出WithTimeout就是一个WithDeadline简化版
}
这也是开发中常用的方法,将方法分层,底层的方法很灵活,但是使用较麻烦,对常用的功能进行简单的封装,方便开发。
WithDeadline
// WithDeadline 返回一个父 Context 的副本,并把它的截止时间调整为不晚于 d。
// 如果父 Context 的截止时间已经早于 d,
// 那么 WithDeadline(parent, d) 在语义上等价于 parent。
// 返回的 Context.Done channel 会在以下任一情况发生时关闭:
// - 截止时间 d 到期
// - 调用了返回的 cancel 函数
// - 父 Context 的 Done channel 被关闭
// 以上三者以最先发生的为准。
//
// 取消这个 Context 会释放与之关联的资源,
// 因此在使用该 Context 的操作完成后,应尽快调用 cancel。
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
return WithDeadlineCause(parent, d, nil)
}
功能遵从朴素的逻辑,没有什么特别的case,和方法名语义一致的方法。
WithDeadlineCause
// WithDeadlineCause 的行为类似于 [WithDeadline],
// 但当超过截止时间时,会把返回的 Context 的取消原因设置为 cause。
// 注意:返回的 [CancelFunc] 不会设置 cause,只会返回 context.Canceled。
func WithDeadlineCause(parent Context, d time.Time, cause error) (Context, CancelFunc) {
if parent == nil {
// 父 Context 不能为 nil,否则直接 panic
// panic("父Context不能为nil")
panic("cannot create context from nil parent")
}
// 检查父context是否已经有更早的截止时间
if cur, ok := parent.Deadline(); ok && cur.Before(d) {
// The current deadline is already sooner than the new one
// 直接返回一个普通的可取消context,因为父context会更早触发取消
return WithCancel(parent)
}
// 创建一个带有截止时间的 timerCtx
c := &timerCtx{
deadline: d,
}
// 建立父子关系,让父 Context 取消时能级联取消当前 c
c.cancelCtx.propagateCancel(parent, c)
// 计算当前时间到 d 的剩余时长
dur := time.Until(d)
if dur <= 0 {
// 如果截止时间已经过去,立刻取消,并把 cause 作为取消原因
c.cancel(true, DeadlineExceeded, cause)
// 返回 Context,以及 cancel 函数(手动 cancel 时不会设置 cause)
return c, func() { c.cancel(false, Canceled, nil) }
}
// 如果还没超时,设置定时器,在 d 到期时自动触发 cancel
c.mu.Lock()
defer c.mu.Unlock()
if c.err == nil { // 还没有被取消
c.timer = time.AfterFunc(dur, func() {// 所以最底层是依赖于time.AfterFunc这个方法的
// 到时间时触发取消,并写入 cause
c.cancel(true, DeadlineExceeded, cause)
})
}
// 返回 Context 和 cancel 函数
// 注意:手动调用 cancel() 时,原因总是 context.Canceled,而不是 cause
return c, func() { c.cancel(true, Canceled, nil) }
}
withoutCancelCtx
// WithoutCancel 返回一个 parent 的副本,
// 但当 parent 被取消时,它不会跟着被取消。
// 换句话说,这个 Context 不会继承父 Context 的取消信号。
//
// 返回的 context 没有 Deadline 或 Err,Done channel 也永远是 nil。
// 调用 [Cause] 时,总是返回 nil。
func WithoutCancel(parent Context) Context {
if parent == nil {
// 父 Context 不能为 nil,否则直接 panic
panic("不能从 nil 父 Context 创建 Context")
}
// 返回一个包装后的 withoutCancelCtx,
// 它屏蔽了 parent 的取消、超时、错误等特性,
// 但仍然可以从 parent 中获取 Value。
return withoutCancelCtx{parent}
}
WithoutCancel - 取消隔离
- 目的:阻断取消信号的传播,创建独立的context
- 特点:继承父context的值但不受其取消影响
- 使用场景:后台任务、日志记录、清理操作等需要独立生命周期的场景
// 用于创建不受父context取消影响的新context
ctx = context.WithoutCancel(parentCtx)
valueCtx
WithValue()
是唯一可以用来传值的方法,而这个结构体就是方法WithValue()
的基石,源码如下:
// 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 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 返回父context的一个副本,其中关联key的值为val。
//
// context Values 仅应用于跨进程和API传递的请求范围数据,而不应用于向函数传递可选参数。
//
// 提供的key必须是可比较的,并且不应是string类型或任何其他内置类型,以避免使用context的包之间发生冲突。
// WithValue的用户应该为自己的key定义自定义类型。为了避免在分配给interface{}时分配内存,
// context key通常应具有具体类型struct{}。或者,导出的context key变量的静态类型应为指针或接口。
func WithValue(parent Context, key, val any) Context {
// 参数校验:确保父context不为nil,避免创建无效的context链
if parent == nil {
panic("cannot create context from nil parent")
}
// 参数校验:key不能为nil,避免无法进行值查找
if key == nil {
panic("nil key")
}
// 参数校验:key必须是可比较的类型,因为context需要支持值的查找操作
if !reflectlite.TypeOf(key).Comparable() {
panic("key is not comparable")
}
// 创建并返回valueCtx实例,包装父context并存储键值对
// valueCtx结构:{parent Context, key, val any}
return &valueCtx{parent, key, val}
}
WithValue的逻辑是:父ctx放入的值,子context可以用;但反过来不行。可以看源码:
func (c *valueCtx) Value(key any) any {
if c.key == key {
return c.val
}
return value(c.Context, key) // 找不到的话,递归向父节点查找
}
func value(c Context, key any) any {
for { // 这是一个for,用于c的类型转化后去到对应的case
switch ctx := c.(type) {
case *valueCtx:
if key == ctx.key {
return ctx.val
}
c = ctx.Context // 对于valueCtx而言,就是不断向上找值
case *cancelCtx:
if key == &cancelCtxKey {
return c
}
c = ctx.Context
case withoutCancelCtx:
if key == &cancelCtxKey {
// This implements Cause(ctx) == nil
// when ctx is created using WithoutCancel.
return nil
}
c = ctx.c
case *timerCtx:
if key == &cancelCtxKey {
return &ctx.cancelCtx
}
c = ctx.Context
case backgroundCtx, todoCtx:
return nil
default:
return c.Value(key)
}
}
}
参考:
[1] https://segmentfault.com/a/1190000040917752#item-3
[2] Go 并发控制:context 源码解读 - 知乎