必须显式调用 cancel(),否则 context.withtimeout 会泄漏定时器和 goroutine;超时仅关闭 ctx.done() 并返回 context.deadlineexceeded,但底层 time.timer 未停止,其 goroutine 持有引用阻碍 gc,需始终 defer cancel() 释放资源。

必须显式调用 cancel(),否则 context.WithTimeout 会泄漏定时器和 goroutine。
为什么 context.WithTimeout 超时后还要手动调用 cancel()
超时触发只是关闭 ctx.Done() 并让 ctx.Err() 返回 context.DeadlineExceeded,但底层 time.Timer 仍在运行,其 goroutine 持有引用无法被 GC 回收。不调用 cancel() 就等于放任一个定时器长期存活。
- 常见错误写法:
ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)—— 忘记接收cancel函数 - 正确姿势:始终用两个变量接收,
ctx, cancel := context.WithTimeout(...) - 无论是否已超时,都应在作用域结束前调用
cancel(),典型做法是defer cancel() - 提前完成任务时也应调用
cancel(),避免资源等待到超时才释放
select 是监听 ctx.Done() 的唯一安全方式
ctx.Done() 是一个永远非 nil 的只读 channel,它只在取消时关闭;ctx.Err() 在取消前恒为 nil。任何基于 if ctx.Done() != nil 或 for ctx.Err() == nil 的轮询都是错的。
- 正确写法只有
select非阻塞监听:case - 在循环中反复检查
ctx.Err()会导致响应延迟,错过及时退出时机 - 对
http.Client.Do等阻塞调用,必须配合select+ctx.Done()实现主动中断,不能只等函数返回
跨服务调用时上下文必须逐层透传
微服务链路中,上游取消信号要能抵达下游所有协程,前提是每个中间环节都把 ctx 作为第一个参数传递,并在调用下游时携带它。
- 漏传
ctx(比如调用doSomething()而不是doSomething(ctx))会导致该分支完全脱离控制 -
context.WithValue创建的上下文本身不可取消,只能靠父级传播取消信号,所以不能用它替代WithCancel或WithTimeout - 多个 goroutine 共享同一个
ctx安全,但共享同一个cancel函数不安全——只能由一个 goroutine 调用
WithValue 的 key 不能用 string
用字符串作 key 极易引发冲突:不同包可能无意使用相同字符串,导致值被覆盖或读错。Go 官方明确建议用自定义类型做 key。
- 推荐做法:定义空结构体类型,如
type traceIDKey struct{},然后用context.WithValue(ctx, traceIDKey{}, "abc123") - 导出 key 类型(首字母大写)便于跨包使用,但更稳妥的是提供封装函数,如
WithTraceID(ctx, id)和GetTraceID(ctx) -
WithValue只适合传请求元数据(如用户 ID、trace ID),绝不该用来传业务逻辑参数或配置
最常被忽略的一点:超时时间精度受 Go runtime 调度影响,单元测试里用 1 * time.Nanosecond 模拟超时几乎必然失败;真实场景中应预留至少几毫秒缓冲,或改用 time.AfterFunc 手动触发来验证逻辑。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











