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{}
}
这个接口主要被三个类继承实现,分别是emptyCtx、ValueCtx、cancelCtx
根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)
}
}
withDeadline、WithTimeout的实现
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()
}