Golang Context

Golang Context

解决问题:当一个任务被取消或超时时,如何通知所有相关的协程(Goroutine)一起停止工作,避免资源浪费

1.Context 的四种核心形态

函数 用途 场景
WithCancel 手动取消 用户关闭了网页,后端立即停止所有关联的计算任务
WithDeadline 定时取消 规定任务必须在“具体某个时间点”(如 18:00)前完成。
WithTimeout 超时取消 规定任务最多执行多久(如 500ms),常用于接口调用。
WithValue 传递元数据 在链路中传递 TraceID、用户鉴权 Token 等。

2.核心原理:树状结构

Context 的设计本质上是一棵由上至下的树

  • 根节点:通常由 context.Background() 创建。
  • 派生节点:通过上述四个函数衍生出的子 Context。
  • 联动效应:如果父 Context 被取消,所有子 Context 都会被递归取消。但子 Context 被取消,不会影响父 Context。

3.代码实战:如何优雅地处理超时

超时后让所有协程都退出

func handleRequest(ctx context.Context) {
    // 派生一个带 2 秒超时的上下文
    ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
    defer cancel() // 记得释放资源!

    go doSomething(ctx)

    select {
    case <-ctx.Done():
        // 如果超时或手动调用了 cancel(),这里会收到信号
        fmt.Println("HandleRequest 结束:", ctx.Err())
    }
}

func doSomething(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            fmt.Println("子任务收到取消信号,准备退出...")
            return
        default:
            // 模拟正在干活
            time.Sleep(500 * time.Millisecond)
        }
    }
}

源码赏析

Context接口

type Context interface {
    //当Context自动取消或者到了取消时间被取消后返回
 Deadline() (deadline time.Time, ok bool)
    //当Context被取消或者到了deadline返回一个被关闭的channel
 Done() <-chan struct{}
    //当Context被取消或者关闭后,返回context取消的原因
 Err() error
    //获取设置的key对应的值
 Value(key interface{}) interface{}
}

这个接口主要被三个类继承实现,分别是emptyCtxValueCtxcancelCtx

Context

var (
    background = new(emptyCtx)
    todo       = new(emptyCtx)
)

func Background() Context {
    return background
}

func TODO() Context {
    return todo
}

emptyCtx

其实现方法是一个空结构

type emptyCtx int

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 interface{}) interface{} {
    return nil
}

func (e *emptyCtx) String() string {
    switch e {
    case background:
        return "context.Background"
    case todo:
        return "context.TODO"
    }
    return "unknown empty Context"
}

//withValue 内部主要就是 valueCtx
func WithValue(parent Context, key, val interface{}) Context {
    return &valueCtx{parent, key, val}
}
//withValue 内部主要就是 cancelCtx
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
    c := newCancelCtx(parent)
    propagateCancel(parent, &c)
    return &c, func() { c.cancel(true, Canceled) }
}

valueCtx

type valueCtx struct {
    Context
    key, val interface{}
}

//在调用Context中的Value方法时会层层向上调用直到最终的根节点
func (c *valueCtx) Value(key interface{}) interface{} {
    if c.key == key {
        return c.val
    }
    return c.Context.Value(key)
}

cancelCtx

type cancelCtx struct {
    Context
	// 互斥锁
    mu       sync.Mutex            
    // 用来做context的取消通知信号 
    done     atomic.Value          
    // key是接口类型canceler,目的就是存储实现当前canceler接口的子节点,当根节点发生取消时,遍历子节点发送取消信号
    children map[canceler]struct{} 
    err      error
}

propagateCancel

func propagateCancel(parent Context, child canceler) {
  // 如果返回nil,说明当前父`context`从来不会被取消,是一个空节点,直接返回即可。
    done := parent.Done()
    if done == nil {
        return // parent is never canceled
    }

  // 提前判断一个父context是否被取消,如果取消了也不需要构建关联了,
  // 把当前子节点取消掉并返回
    select {
    case <-done:
        // parent is already canceled
        child.cancel(false, parent.Err())
        return
    default:
    }

  // 这里目的就是找到可以“挂”、“取消”的context
    if p, ok := parentCancelCtx(parent); ok {
        p.mu.Lock()
    // 找到了可以“挂”、“取消”的context,但是已经被取消了,那么这个子节点也不需要
    // 继续挂靠了,取消即可
        if p.err != nil {
            child.cancel(false, p.err)
        } else {
      // 将当前节点挂到父节点的childrn map中,外面调用cancel时可以层层取消
            if p.children == nil {
        // 这里因为childer节点也会变成父节点,所以需要初始化map结构
                p.children = make(map[canceler]struct{})
            }
            p.children[child] = struct{}{}
        }
        p.mu.Unlock()
    } else {
    // 没有找到可“挂”,“取消”的父节点挂载,那么就开一个goroutine
        atomic.AddInt32(&goroutines, +1)
        go func() {
            select {
            case <-parent.Done():
                child.cancel(false, parent.Err())
            case <-child.Done():
            }
        }()
    }
}

cancel方法

func (c *cancelCtx) cancel(removeFromParent bool, err error) {
  // 取消时传入的error信息不能为nil, context定义了默认error:var Canceled = errors.New("context canceled")
    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
  // 用来关闭channel,通知其他协程
    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)
    }
  // 节点置空
    c.children = nil
    c.mu.Unlock()
  // 把当前节点从父节点中移除,只有在外部父节点调用时才会传true
  // 其他都是传false,内部调用都会因为c.children = nil被剔除出去
    if removeFromParent {
        removeChild(c.Context, c)
    }
}

withDeadlineWithTimeout的实现

WithTimeout调用了WithDeadline

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
  // 不能为空`context`创建衍生context
    if parent == 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.
        return WithCancel(parent)
    }
  // 创建一个timerCtx对象
    c := &timerCtx{
        cancelCtx: newCancelCtx(parent),
        deadline:  d,
    }
  // 将当前节点挂到父节点上
    propagateCancel(parent, c)
  
  // 获取过期时间
    dur := time.Until(d)
  // 当前时间已经过期了则直接取消
    if dur <= 0 {
        c.cancel(true, DeadlineExceeded) // deadline has already passed
        return c, func() { c.cancel(false, Canceled) }
    }
    c.mu.Lock()
    defer c.mu.Unlock()
  // 如果没被取消,则直接添加一个定时器,定时去取消
    if c.err == nil {
        c.timer = time.AfterFunc(dur, func() {
            c.cancel(true, DeadlineExceeded)
        })
    }
    return c, func() { c.cancel(true, Canceled) }
}

withDeadline相较于withCancel方法也就多了一个定时器去定时调用cancel方法,这个cancel方法在timerCtx类中进行了重写,我们先来看一下timerCtx类,他是基于cancelCtx的,多了两个字段:

type timerCtx struct {
    cancelCtx
    timer *time.Timer // Under cancelCtx.mu.

    deadline time.Time
}
//timerCtx实现的cancel方法,内部也是调用了cancelCtx的cancel方法取消
func (c *timerCtx) cancel(removeFromParent bool, err error) {
  // 调用cancelCtx的cancel方法取消掉子节点context
    c.cancelCtx.cancel(false, err)
  // 从父context移除放到了这里来做
    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()
}