Golang 之 Context 源码解析(1.20+)
文章目录
- Golang 之 Context 源码解析
- 一、Context 包核心结构与接口
- 1. 核心接口 `Context`
- 2. 可取消接口 `canceler`
- 二、创建根Context
- emptyCtx
- 三、context的继承衍生
- WithValue嵌入数据
- withValue注意事项
- value context的源码
- value context的底层是map吗?
- WithCancel取消控制
- WithCancel源码
- 1. 方法签名和参数检查
- 2. 加锁和重复取消检查
- 3. 设置取消状态
- 4. 关闭 done 通道
- 5. 递归取消所有子上下文
- 6. 解锁
- 7. 从父上下文中移除(如果需要)
- afterFuncCtx示例
- afterFuncCtx源码
- withDeadline、WithTimeout 超时控制
- withDeadline、WithTimeout 源码分析
- 四、树形结构设计原因
- 五、context使用原则
- 六、参考资料
Golang 之 Context 源码解析
上下文 context.Context
是 Go 语言中用于设置截止日期、同步信号、传递请求相关值的结构体。它与 Goroutine 关系密切,是 Go 语言的独特设计,在其他编程语言中较少见到类似概念。
Go 官方文档对 context
包的定义为:
“A Context carries a deadline, a cancelation signal, and other values across API boundaries.”
即一个上下文携带着到期时间、取消信号及其他值跨越 API 的界线。
一、Context 包核心结构与接口
1. 核心接口 Context
type Context interface {Deadline() (deadline time.Time, ok bool)Done() <-chan struct{}Err() errorValue(key interface{}) interface{}
}
Deadline()
:获取超时时间及是否已设置超时时间。Done()
:返回一个 Channel,在 Context 被取消或超时时关闭。Err()
:返回 Context 结束的原因(Canceled 或 DeadlineExceeded)。Value(key)
:从 Context 中获取键对应的值,按树状结构向上查找。
2. 可取消接口 canceler
type canceler interface {cancel(removeFromParent bool, err, cause error)Done() <-chan struct{}
}
实现类型:*cancelCtx
和 *timerCtx
(均为指针类型)。
二、创建根Context
emptyCtx
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
}
- 私有结构体context.emptyCtx是最简单、最常用的上下文类型,context.emptyCtx 通过返回 nil 实现了 context.Context 接口,它没有任何特殊的功能。
- context包允许2种方式创建和获得一个初始的context:
- context.Background()
- context.TODO()
- 这两个方法都会返回预先初始化好的私有变量background和todo,其实就是一个指向int类型全局变量的指针,它们会在同一个 Go 程序中被复用:
type backgroundCtx struct{ emptyCtx }func (backgroundCtx) String() string {return "context.Background"
}type todoCtx struct{ emptyCtx }func (todoCtx) String() string {return "context.TODO"
}func Background() Context {return backgroundCtx{}
}func TODO() Context {return todoCtx{}
}
- 从源代码来看,context.Background和context.TODO也只是互为别名,没有太大的差别,只是在使用和语义上稍有不同:
- context.Background是上下文的默认值,所有其他的上下文都应该从它衍生出来;
- context.TODO应该仅在不确定应该使用哪种上下文时使用;
- 在多数情况下,如果当前函数没有上下文作为入参,我们都会使用context.Background作为起始的上下文向下传递。
三、context的继承衍生
有了如上的根Context,那么是如何衍生更多的子Context的呢?这就要靠context包为我们提供的With系列的函数了:
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
这四个With函数,接收的都有一个parent参数,就是父Context,我们要基于这个父Context创建出子Context的意思,这种方式可以理解为子Context对父Context的继承,也可以理解为基于父Context的衍生。
- WithCancel():传递一个父Context作为参数,返回子Context,以及一个取消函数用来取消Context
- WithDeadline():和WithCancel()差不多,它会多传递一个截止时间参数,意味着到了这个时间点,会自动取消Context,当然我们也可以不等到这个时候,可以提前通过取消函数进行取消。
- WithTimeout():和WithDeadline()基本上一样,这个表示是超时自动取消,是多少时间后自动取消Context的意思。
- WithValue():函数和取消Context无关,它是为了生成一个绑定了一个键值对数据的Context,这个绑定的数据可以通过Context.Value()方法访问到。
大家可能留意到,前三个函数都返回一个取消函数 CancelFunc,这是一个函数类型,它的定义非常简单。
type CancelFunc func()
这就是取消函数的类型,该函数可以取消一个Context,以及这个节点Context下所有的Context,不管有多少层级。
通过这些函数,就创建了一颗Context树,树的每个节点都可以有任意多个子节点,节点层级可以有任意多个。
WithValue嵌入数据
package main import ( "context" "fmt"
) type orderID int func main() { var x = context.TODO() x = context.WithValue(x, orderID(1), "1234") x = context.WithValue(x, orderID(2), "2345") y := context.WithValue(x, orderID(3), "4567") x = context.WithValue(x, orderID(3), "3456") fmt.Println(x.Value(orderID(3))) fmt.Println(y.Value(orderID(3)))
}
就是像下面这样的图了:
┌────────────┐ │ emptyCtx │ └────────────┘ ▲ │ │ │ parent │ │ ┌───────────────────────────────────┐ │ valueCtx{k: 1, v: 1234} │ └───────────────────────────────────┘ ▲ │ │ │ parent │ │ │ ┌───────────────────────────────────┐ │ valueCtx{k: 2, v: 2345} │ └───────────────────────────────────┘ ▲ │ ┌──────────────┴──────────────────────────┐ │ │ │ │
┌───────────────────────────────────┐ ┌───────────────────────────────────┐
│ valueCtx{k: 3, v: 3456} │ │ valueCtx{k: 3, v: 4567} │
└───────────────────────────────────┘ └───────────────────────────────────┘┌───────┐ ┌───────┐ │ x │ │ y │ └───────┘ └───────┘
我们基于context.Background创建一个携带trace_id的ctx,然后通过context树一起传递,从中派生的任何context都会获取此值,我们最后打印日志的时候就可以从ctx中取值输出到日志中。目前一些RPC框架都是支持了Context,所以trace_id的向下传递就更方便了。
withValue注意事项
- 不建议使用context值传递关键参数,关键参数应该显示的声明出来,不应该隐式处理,context中最好是携带签名、trace_id这类值。
- 因为携带value也是key、value的形式,为了避免context因多个包同时使用context而带来冲突,key建议采用内置类型。
- 上面的例子我们获取trace_id是直接从当前ctx获取的,实际我们也可以获取父context中的value,在获取键值对是,我们先从当前context中查找,没有找到会在从父context中查找该键对应的值直到在某个父context中返回nil或者查找到对应的值。
- context传递的数据中key、value都是interface类型,这种类型编译期无法确定类型,所以不是很安全,所以在类型断言时别忘了保证程序的健壮性。
value context的源码
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}
} type valueCtx struct { Context key, val any
} func stringify(v any) string { switch s := v.(type) { case stringer: return s.String() case string: return s case nil: return "<nil>" } return reflectlite.TypeOf(v).String()
} func (c *valueCtx) String() string { return contextName(c.Context) + ".WithValue(" + stringify(c.key) + ", " + stringify(c.val) + ")"
} 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 { 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) } }
}
value context的底层是map吗?
-
在上面valueCtx的定义中,我们可以看出其实value context底层不是一个map,而是每一个单独的kv映射都对应一个valueCtx,当传递多个值时就要构造多个ctx。同时,这就是value context不能自底向上传递值的原因。
-
valueCtx的key、val都是接口类型,在调用WithValue的时候,内部会首先通过反射确定key是否可比较类型(同map中的key),然后赋值key
-
在调用Value的时候,内部会首先在本context查找对应的key,如果没有找到会在parent context中递归寻找,这也是value可以自顶向下传值的原因。
-
value context的递归调用在查找值,即执行 Value 操作时,会先判断当前节点的 k 是不是等于用户的输入 k,如果相等,返回结果,如果不等,会依次向上从子节点向父节点,一直查找到整个 ctx 的根。没有找到返回 nil。是一个递归流程。
-
通过分析,ctx 这么设计是为了能让代码每执行到一个点都可以根据当前情况嵌入新的上下文信息,但我们也可以看到,如果我们每次加一个新值都执行 WithValue 会导致 ctx 的树的层数过高,查找成本比较高 O(H)。
-
很多业务场景中,我们希望在请求入口存入值,在请求过程中随时取用。这时候我们可以将 value 作为一个 map 整体存入。
context.WithValue(context.Background(), info, map[string]string{"order_id" : "111", "payment_id" : "222"}
)
WithCancel取消控制
-
日常业务开发中我们往往为了完成一个复杂的需求会开多个gouroutine去做一些事情,这就导致我们会在一次请求中开了多个goroutine确无法控制他们,这时我们就可以使用withCancel来衍生一个context传递到不同的goroutine中,当我想让这些goroutine停止运行,就可以调用cancel来进行取消。
-
来看一个例子:
func main() { ctx,cancel := context.WithCancel(context.Background()) go Speak(ctx) time.Sleep(10*time.Second) cancel() time.Sleep(1*time.Second)
} func Speak(ctx context.Context) { for range time.Tick(time.Second){ select { case <- ctx.Done(): fmt.Println("我要闭嘴了") return default: fmt.Println("balabalabalabala") } }
}
运行结果:
balabalabalabala
....省略
balabalabalabala
我要闭嘴了
- 我们使用withCancel创建一个基于Background的ctx,然后启动一个讲话程序,每隔1s说一话,main函数在10s后执行cancel,那么speak检测到取消信号就会退出。
WithCancel源码
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) // 构建父子上下文之间的关联,当父上下文被取消时,子上下文也会被取消 return c
} type cancelCtx struct { Context // 父上下文 mu sync.Mutex // 保护以下字段 done atomic.Value // 存储 chan struct{},惰性创建,第一次取消时关闭 children map[canceler]struct{} // 第一次取消时置为 nil err error // 第一次取消时设置为非 nil cause error // 第一次取消时设置为非 nil
} // 构建父子上下文之间的关联,当父上下文被取消时,子上下文也会被取消
func (c *cancelCtx) propagateCancel(parent Context, child canceler) { c.Context = parent done := parent.Done() if done == nil { return // 父上下文不会触发取消信号,比如parent是emptyCtx } select { case <-done: // 父上下文已经被取消 child.cancel(false, parent.Err(), Cause(parent)) return default: } if p, ok := parentCancelCtx(parent); ok { // 如果父上下文是标准 cancelCtx 类型(或派生自它) p.mu.Lock() if p.err != nil { // 检查父上下文是否已取消,如果是,取消子上下文 child.cancel(false, p.err, p.cause) } else { // 否则,将子上下文添加到父上下文的 children 集合中,建立父子关系 if p.children == nil { p.children = make(map[canceler]struct{}) } p.children[child] = struct{}{} } p.mu.Unlock() return } if a, ok := parent.(afterFuncer); ok { // 如果父上下文实现了 AfterFunc 方法 // 注册一个回调,在父上下文取消时取消子上下文 // 将当前上下文包装为 stopCtx,以便后续可以停止回调 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 } // 对于其他类型的父上下文,启动一个 goroutine 来监听 // 如果父上下文取消,则取消子上下文 // 如果子上下文先被取消,则 goroutine 退出 goroutines.Add(1) go func() { select { case <-parent.Done(): child.cancel(false, parent.Err(), Cause(parent)) case <-child.Done(): } }()
}
- 通过源码我们可以知道cancel方法可以被重复调用,是幂等的。
var Canceled = errors.New("context canceled") // 取消当前上下文,并递归地取消所有子上下文,同时可以选择从父上下文中移除自己
func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) { if err == nil { panic("context: internal error: missing cancel error") } if cause == nil { cause = err } c.mu.Lock() if c.err != nil { c.mu.Unlock() return // already canceled } c.err = err c.cause = cause d, _ := c.done.Load().(chan struct{}) if d == nil { c.done.Store(closedchan) } else { close(d) } for child := range c.children { // NOTE: acquiring the child's lock while holding parent's lock. child.cancel(false, err, cause) } c.children = nil c.mu.Unlock() if removeFromParent { removeChild(c.Context, c) }
}
cancel
方法的主要作用是取消当前上下文,并递归地取消所有子上下文,同时可以选择从父上下文中移除自己。
1. 方法签名和参数检查
var Canceled = errors.New("context canceled")func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {
removeFromParent
: 布尔值,表示是否从父上下文中移除当前上下文err
: 取消的错误原因,通常为Canceled
cause
: 取消的根本原因(Go 1.20+ 引入)
if err == nil {panic("context: internal error: missing cancel error")
}
if cause == nil {cause = err
}
- 必须提供取消错误,否则 panic
- 如果没有提供 cause,则使用 err 作为 cause
2. 加锁和重复取消检查
c.mu.Lock()
if c.err != nil {c.mu.Unlock()return // already canceled
}
- 加锁保护并发操作
- 如果已经取消过(c.err != nil),直接返回
3. 设置取消状态
c.err = err
c.cause = cause
- 记录取消错误和原因
4. 关闭 done 通道
d, _ := c.done.Load().(chan struct{})
if d == nil {c.done.Store(closedchan)
} else {close(d)
}
done
是原子值,存储一个 chan struct{}- 如果还没创建过 done 通道,使用预定义的
closedchan
(一个已关闭的全局通道) - 如果已创建,则关闭它(通知所有监听者)
5. 递归取消所有子上下文
for child := range c.children {// NOTE: acquiring the child's lock while holding parent's lock.child.cancel(false, err, cause)
}
c.children = nil
- 遍历所有子上下文,逐个取消它们
- 传递
false
表示子上下文不需要从当前上下文中移除(因为当前上下文即将被清空) - 最后清空 children 集合
6. 解锁
c.mu.Unlock()
- 完成所有操作后解锁
7. 从父上下文中移除(如果需要)
if removeFromParent {removeChild(c.Context, c)
}
- 如果
removeFromParent
为 true,调用removeChild
从父上下文中移除当前上下文
afterFuncCtx示例
afterFuncCtx是1.20版本后引入的新ctx,它的作用是当ctx被取消后,能执行一次自定义的函数,一般用于回收资源等。
package main import ( "context" "fmt" "time"
) func main() { // 创建一个 2 秒后超时的 context ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() // 注册 AfterFunc,context 完成时将调用清理操作 stop := context.AfterFunc(ctx, func() { fmt.Println("清理操作正在执行...") }) // 等待 3 秒 time.Sleep(3 * time.Second) // 尝试停止清理操作 stopped := stop() if stopped { fmt.Println("成功停止清理操作") } else { fmt.Println("清理操作已经开始或已停止") } // 等待 context 超时 <-ctx.Done() fmt.Println("程序结束")
}
执行流程:
- 0秒:程序启动,创建2秒超时的context,注册AfterFunc回调。
- 2秒:context超时,触发AfterFunc回调,输出“清理操作正在执行…”。
- 3秒:主程序从sleep中醒来,调用
stop()
但回调已执行,输出“清理操作已经开始或已停止”,随后输出“程序结束”。
afterFuncCtx源码
afterFuncCtx是1.20版本后引入的新ctx,作用是当ctx被取消后执行一次自定义函数(用于回收资源等)。
type afterFuncCtx struct { cancelCtx once sync.Once // 确保函数要么执行一次,要么被停止 f func() // 待执行的回调函数
} func AfterFunc(ctx Context, f func()) (stop func() bool) { a := &afterFuncCtx{ f: f, } a.cancelCtx.propagateCancel(ctx, a) // 建立父子上下文关联 return func() bool { // 返回闭包函数用于停止回调 stopped := false a.once.Do(func() { // 保证仅执行一次停止逻辑 stopped = true }) if stopped { a.cancel(true, Canceled, nil) // 停止后取消上下文 } return stopped // true:成功取消;false:回调已执行或正在执行 }
}
闭包原理:
AfterFunc
返回的stop
函数引用了局部变量a
,形成闭包,使a
的生命周期延长至stop
函数不再被引用。- 闭包捕获
a
,即使AfterFunc
方法返回,a
仍存在于堆上,确保stop
可操作a
的状态。
withDeadline、WithTimeout 超时控制
package main import ( "context" "fmt" "time"
) func main() { ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond) defer cancel() select { case <-time.After(1 * time.Second): fmt.Println("overslept") case <-ctx.Done(): fmt.Println(ctx.Err()) // 输出 "context deadline exceeded" }
}
withDeadline、WithTimeout 源码分析
timerCtx
继承自 cancelCtx
,通过定时器和截止时间实现超时取消:
type timerCtx struct { cancelCtx timer *time.Timer // 定时器,用于触发超时 deadline time.Time // 截止时间
} func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) { return WithDeadline(parent, time.Now().Add(timeout)) // 超时时间转换为截止时间
} func WithDeadlineCause(parent Context, d time.Time, cause error) (Context, CancelFunc) { if parent == nil { panic("cannot create context from nil parent") } c := &timerCtx{deadline: d} c.cancelCtx.propagateCancel(parent, c) dur := time.Until(d) if dur <= 0 { // 截止时间已过,直接取消 c.cancel(true, DeadlineExceeded, cause) return c, func() { c.cancel(false, Canceled, nil) } } c.mu.Lock() defer c.mu.Unlock() if c.err == nil { // 启动定时器,超时后触发取消 c.timer = time.AfterFunc(dur, func() { c.cancel(true, DeadlineExceeded, cause) }) } return c, func() { c.cancel(true, Canceled, nil) } // 取消函数同时停止定时器
} // timerCtx.cancel 方法:停止定时器并触发取消
func (c *timerCtx) cancel(removeFromParent bool, err, cause error) { c.cancelCtx.cancel(false, err, cause) if removeFromParent { removeChild(c.cancelCtx.Context, c) } c.mu.Lock() if c.timer != nil { c.timer.Stop() // 停止定时器,避免资源浪费 c.timer = nil } c.mu.Unlock()
}
四、树形结构设计原因
- 匹配程序执行模型:Go的并发模型(如
go func
)本质是树形分叉(fork-join),上下文的链式树形结构可自然匹配代码执行流程。 - 数据隔离与继承:子节点可在父节点基础上添加新数据(如
WithValue
),且不影响父节点,符合“自顶向下传递”的设计原则。
五、context使用原则
- 传递方向:只能自顶向下传值,不能反向传递。
- 根节点:
context.Background()
作为最高层级根节点,派生所有子context。 - 取消机制:取消是建议性的,函数需响应取消信号并优雅退出。
- 结构体设计:不要将Context放在结构体中,应作为函数参数传递。
- 参数顺序:以Context为参数的函数,应将其作为第一个参数。
- 非空原则:禁止传递
nil
Context,不确定时使用context.TODO()
。 - 值传递限制:
Context.Value
仅用于传递必需的元数据(如trace_id
),避免传递可选参数(显式参数更安全)。 - 线程安全:Context可安全地在多个goroutine中传递,支持并发访问。
- 取消函数调用:若有
CancelFunc
,必须确保调用(如defer cancel()
),避免资源泄露(如定时器未停止)。
六、参考资料
- https://www.cnblogs.com/MelonTe/p/18493295#0%E5%89%8D%E8%A8%80
- https://zhuanlan.zhihu.com/p/520323175
- https://github.com/cch123/golang-notes/blob/master/context.md
- https://segmentfault.com/a/1190000040917752#item-1
- https://draven.co/golang/docs/part3-runtime/ch06-concurrency/golang-context/#61-%E4%B8%8A%E4%B8%8B%E6%96%87-context