引子
今天心血来潮,给自己梳理梳理golang中这个十分重要的东东——context,毕竟也是在面试中考察到,但是反问自己的时候却感觉一点也说不出来什么有价值的东西,所以今天我来一次刨根究底。

源码解析
Package context defines the Context type, which carries deadlines, cancellation signals, and other request-scoped values across API boundaries and between processes.
这是来自于官方的最新说明,意味着 context 携带终止期限,取消信号,以及跨API,进程之间通信等信息。
即context设计的核心目标是
- 任务取消机制:跨API和Goroutine的任务取消信号传播机制
- 超时控制:超时或截止时间控制任务生命周期
- 数据共享:在不同函数调用间共享元数据
明确了这些,我们进入到源码中。
Context接口及错误变量
下面这段是来自于Chatgpt的精简版(去掉了长长的英文注释),可以发现Context是一个接口,其中包含了四个方法等待实现。context.Context
其中第二个比较特殊,返回的是一个channel。
type Context interface {
Deadline() (deadline time.Time, ok bool) // 返回任务截止时间
Done() <-chan struct{} // 返回取消信号的 channel
Err() error // 返回上下文取消的原因
Value(key any) any // 获取上下文中的值
}
- Done 返回一个仅接收的内部通道,当取消信号发到通道后,此通道得关闭,同时context取消;
- 关闭通道是唯一一个所有消费者 goroutine 都能够感知到的通道操作
case <-ctx.Done()
- 如果 Done 通道还未关闭,Err 返回 nil;如果关闭了,就会返回其关闭原因,具体见下面的两个错误类型变量
ctx.Err()
说到这里我们需要补充一下,整个context包中一共有以下这些上下文类型,接下来就会根据这几种类型分别阐述。
- emptyCtx
- cancleCtx
- timerCtx
- valueCtx
- 以及cancleCtx下的一系列衍生体
定义完了接口,接下来是两个错误类型变量:
var Canceled = errors.New("context canceled")
var DeadlineExceeded error = deadlineExceededError{}
//
type deadlineExceededError struct{}
func (deadlineExceededError) Error() string { return "context deadline exceeded" }
func (deadlineExceededError) Timeout() bool { return true }
func (deadlineExceededError) Temporary() bool { return true }
根据源码中的注释可以得知
- Canceld是上下文被取消时的错误类型,是一个简单的 error 对象,由 errors.New 创建。
- DealineExceeded表示上下文超出截止时间的错误类型,具体实现是 deadlineExceededError,一个实现了 error 接口的结构体,可以为这个错误类型提供更加丰富的信息。
- 下面三个实现的来自于error的接口表示这是由于超时而引起的错误,暂时性的。
看到这里,我想你已经猜出为什么要定义这两个变量了,正是能够适配 context 接口中的 Err() 方法,至于到底怎么具体实现的 Err() 方法我们后面再说。
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
}
显而易见,这是一个空的上下文实现,通常用作上下文链的根节点或者占位符。 注释中也提到,这是一个没有任何状态信息,不包含取消、截止时间、或值存储功能,不能通过取消函数手动取消的上下文。
BackgroundCtx & todoCtx结构
type backgroundCtx struct{ emptyCtx }
func (backgroundCtx) String() string {
return "context.Background"
}
type todoCtx struct{ emptyCtx }
func (todoCtx) String() string {
return "context.TODO"
}
这两个接口就用到了emptyCtx,提供了最基础的上下文功能,为开发中的代码提供一个临时的上下文对象,避免为空。
- Background 通常用于上下文链的根节点。
- todo 通常用作占位符
- 推荐使用 todo,二者在行为上是一致的,但是 todo 的语义更加明确,让人们知道到这是一个待确定的地方;而 background 更常见在顶层的调用上,是根。
对应的还有两个方法来生成二者。
func Background() Context {
return backgroundCtx{}
}
func TODO() Context {
return todoCtx{}
}
cancelCtx:支持显示取消的结构
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
}
func (c *cancelCtx) Value(key any) any {
if key == &cancelCtxKey {
return c
}
return value(c.Context, key)
}
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{})
}
func (c *cancelCtx) Err() error {
c.mu.Lock()
err := c.err
c.mu.Unlock()
return err
}
哦,原来你小子嵌套了Context祖宗,你是儿子啊!然后你还多定义了什么:
- mu,很常见的保护并发访问
- done 存储一个懒加载的通道,上下文被取消时关闭此通道;(这里的atomic.Value先放放)
- children,存储派生于该上下文的子上下文,形成一个上下文链(取消时通知他们)
- err,错误,通常用上面那两个错误变量
- casuse,错误原因。
后面的这几个就是实现Context接口中的方法
- value:如果键是 cancelCtxKey,直接返回 cancelCtx 本身;否则交给父上下文处理
- Done:先从 done.Load 获取已有通道,如果有了,直接返回;如果没有,创建一个新通道并存储(注意加锁后又尝试加载了一次,防止其他协程创建了通道);相同上下文返回唯一通道
- 这是一种双检查锁的模式
- Err:保证线程安全地返回错误状态
接下来是此结构中最重要的 cancel 方法,用于取消当前上下文,并传播取消信号到所有的子上下文,整个流程大致总结如下:
- 设置 err 和 cause 表示上下文已经被取消
- 关闭 Done 通道以通知所有监听者
- 遍历递归取消所有子上下文(链条)
- 最后清理资源
// 是否需要将当前上下文从其父上下文的 children 集合中移除,取消的错误信息,取消的原因
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
// 检查done通道是否已创建,如果没有就存储一个,否则调用close关闭通道
d, _ := c.done.Load().(chan struct{})
if d == nil {
c.done.Store(closedchan)
} else {
close(d)
}
// 至此,通知所有子上下文关闭,递归地调用cancel方法
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)
}
}
CancelFunc函数类型
// A CancelFunc tells an operation to abandon its work.
// A CancelFunc does not wait for the work to stop.
// A CancelFunc may be called by multiple goroutines simultaneously.
// After the first call, subsequent calls to a CancelFunc do nothing.
type CancelFunc func()
是一个函数类型,表示一个无参数、无返回值的函数,并发安全,一次性触发(只有第一次调用会有效)。
之所以这里定义为函数类型,就是为了让其他需要使用到CancelFunc的方法通过闭包来绑定具体的逻辑。稍后我们在一些具体方法中就能看到。
WithCancel & WithCancelCause
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
c := withCancel(parent)
return c, func() { c.cancel(true, Canceled, nil) }
}
type CancelCauseFunc func(cause error)
func WithCancelCause(parent Context) (ctx Context, cancel CancelCauseFunc) {
c := withCancel(parent)
return c, func(cause error) { c.cancel(true, Canceled, cause) }
}
- WithCancel
- 传入父上下文,创建一个取消上下文,返回此上下文和一个取消函数
- 可以发现正如我们上面所说,利用闭包,直接返回了一个取消函数cancel来触发上下文的取消
- WithCancelCause
- 将WithCancel中的CancelFunc换为CancelCasuseFunc,带有取消原因。
- 下面的闭包函数中可以传入一个取消原因
cause error
func withCancel(parent Context) *cancelCtx {
if parent == nil {
panic("cannot create context from nil parent")
}
c := &cancelCtx{}
c.propagateCancel(parent, c)
return c
}
这个withCancel就是上面两个方法分别调用的核心方法来创建一个cancelCtx上下文实例。
先判断父上下文是否为空(上下文链必须有一个根),随后创建一个cancelCtx上下文实例,调用propagateCancel关联父子并监听父上下文的取消事件。
说到这里可能有些懵逼了,那我们就来看看这个 propagateCancel 是怎么个事!

propagateCancel
顾名思义,传播,用于将子取消逻辑与父取消逻辑联系起来形成一个链。具体代码逻辑的解释见注释。
// 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 // parent作为基础(即上面的嵌套Context)
done := parent.Done() // 获取parent的Done通道,记录着取消信号的channel
if done == nil { // 如果为空,说明没有取消信号,parent永远不会取消
return // parent is never canceled
}
select {
case <-done: // ?
// parent is already canceled
child.cancel(false, parent.Err(), Cause(parent))
return
default:
}
// 检查parent类型是否为cancelCtx或派生类型
if p, ok := parentCancelCtx(parent); ok {
// parent is a *cancelCtx, or derives from one.
p.mu.Lock()
if p.err != nil { // 如果错误类型不为空,证明parent已被取消了
// parent has already been canceled
child.cancel(false, p.err, p.cause)
} else { // 否则将child加入到parent的子集中
if p.children == nil {
p.children = make(map[canceler]struct{})
}
p.children[child] = struct{}{}
}
p.mu.Unlock()
return
}
// 如果parent实现了AfterFunc方法,这个之后我们会说,是一种定时回调
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
}
// 用于处理无法直接关联的上下文,goroutine同时监听parent和child的Done通道
goroutines.Add(1)
go func() {
select {
case <-parent.Done():
child.cancel(false, parent.Err(), Cause(parent))
case <-child.Done():
}
}()
}
所以,回到最开始的 withCancel 中,构建了一个取消传播链,可以调用 CancelFunc 来触发取消逻辑,释放资源。
Cause
从Context中提取取消原因(cause)
func Cause(c Context) error {
if cc, ok := c.Value(&cancelCtxKey).(*cancelCtx); ok {
cc.mu.Lock()
defer cc.mu.Unlock()
return cc.cause
}
return c.Err()
}
afterFuncCtx
在cancelCtx的基础上实现回调。
type afterFuncCtx struct {
cancelCtx
once sync.Once // 确保 f 只被执行一次
f func() // 要在上下文完成时调用的回调函数
}
AfterFunc传入回调函数f,并将afterFuncCtx关联到父上下文
func AfterFunc(ctx Context, f func()) (stop func() bool) {
a := &afterFuncCtx{
f: f,
}
// 将 afterFuncCtx 作为子上下文,关联到父上下文 ctx
a.cancelCtx.propagateCancel(ctx, a)
// 返回的 stop 函数用于停止回调的执行
return func() bool {
stopped := false
a.once.Do(func() {
stopped = true
})
if stopped {
a.cancel(true, Canceled, nil)
}
return stopped
}
}
timerCtx
这是cancelCtx的扩展,支持设置截止时间的上下文类型,多了 timer 和 deadline 字段。
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() + "])"
}
// 调用cancelCtx的cancel实现取消,多了定时器
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()
}
WithTimeout & WithDeadline
基于超时持续时间创建上下文;基于指定的截止时间创建上下文
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
return WithDeadlineCause(parent, d, nil)
}
可以发现最终还是调用的是 WithDeadlineCause 这个方法
1.检查父上下文是否有更早的截止时间 2.创建timerCtx 3.计算剩余时间 4.设置定时器 5.返回上下文和取消函数
func WithDeadlineCause(parent Context, d time.Time, cause error) (Context, CancelFunc) {
if parent == nil {
panic("cannot create context from nil parent")
}
if cur, ok := parent.Deadline(); ok && cur.Before(d) {
// The current deadline is already sooner than the new one.
return WithCancel(parent)
}
c := &timerCtx{
deadline: d,
}
// 关联到父上下文并计算截止时间的剩余时间
c.cancelCtx.propagateCancel(parent, c)
dur := time.Until(d)
if dur <= 0 {
c.cancel(true, DeadlineExceeded, cause) // deadline has already passed
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) }
}
valueCtx
实现键值对存储功能的一种上下文类型,适合携带少量元数据。
type valueCtx struct {
Context
key, val any
}
Value:如果当前上下文没有匹配的键,则调用 value 函数继续在父上下文中查找。
value:递归辅助方法,用于在上下文链中查找指定key对应的值
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) {
// 如果是valueCtx类型
case *valueCtx:
if key == ctx.key {
return ctx.val
}
c = ctx.Context //向父上下文继续找
// 如果是cancelCtx类型
case *cancelCtx:
if key == &cancelCtxKey {
return c
}
c = ctx.Context
// 如果是withoutCancelCtx类型
case withoutCancelCtx:
if key == &cancelCtxKey {
// This implements Cause(ctx) == nil
// when ctx is created using WithoutCancel.
return nil
}
c = ctx.c
// 如果是timerCtx类型
case *timerCtx:
if key == &cancelCtxKey {
return &ctx.cancelCtx
}
c = ctx.Context
// 如果是backgroundCtx,todoCtx类型即根上下文
case backgroundCtx, todoCtx:
return nil
default:
return c.Value(key)
}
}
}
withValue方法
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}
}
常用的Go Context 类型对比
| 类型 | 功能描述 | 支持取消 | 支持超时/截止时间 | 支持键值对存储 |
|---|---|---|---|---|
emptyCtx | 根上下文类型。用于创建 context.Background() 和 context.TODO(),无任何功能。 | 否 | 否 | 否 |
cancelCtx | 支持取消功能,通过 WithCancel 或 WithCancelCause 创建,可以传播取消信号。 | 是 | 否 | 否 |
timerCtx | 基于 cancelCtx,支持取消和超时,通过 WithTimeout 和 WithDeadline 创建。 | 是 | 是 | 否 |
valueCtx | 支持键值对存储,通过 WithValue 创建,通常嵌套在其他类型上下文中使用。 | 否 | 否 | 是 |
平时是如何使用的
这里举例说明context以及其中的常见方法在代码中是如何使用的。
WithTimeout&WithDeadline
在这段代码中,context.WithTimeout 起到了控制数据库操作是否超时的作用
func (m MovieModel) Insert(movie *Movie) error {
// 插入一条新记录的SQL语句,并返回信息(Postgresql专有)
query := `
INSERT INTO movies (title, year, runtime, genres)
VALUES ($1, $2, $3, $4)
RETURNING id, created_at, version`
// 创建一个代表着占位符的movie中的属性切片
args := []interface{}{movie.Title, movie.Year, movie.Runtime, pq.Array(movie.Genres)}
// Create a context with a 3-second timeout
// 如果数据库操作在3s内没有完成,操作自动取消,返回超时错误
ctx, cancle := context.WithTimeout(context.Background(), 3*time.Second)
defer cancle()
// 使用QueryRowContext方法执行,利用传入的ctx进行SQL查询,并使用Scan方法将返回值注入到movie的三个属性中
return m.DB.QueryRowContext(ctx, query, args...).Scan(&movie.ID, &movie.CreatedAt, &movie.Version)
}
总结
Golang中经常使用 Context,以上四种 Context 类型各有其作用,通常利用其自带的函数例如WithTimeout(),Background()来进行控制操作。
其中值得注意的是 cancleCtx 的取消机制中的传播链,存在回调机制,值得我多去思考。
这里是LTX,感谢您阅读这篇博客,人生海海,和自己对话,像只蝴蝶纵横四海。