context.withtimeout 必须手动调用 cancel() 以关闭 done channel、唤醒阻塞 goroutine,防止泄漏;context 不可长期持有,应随请求传入新实例;withvalue 的 key 应用未导出类型避免冲突。

context.WithTimeout 为什么必须手动调用 cancel()?
因为 context.WithTimeout 返回的 cancel 函数不只是释放定时器,还会关闭内部的 done channel 并唤醒所有阻塞在 <ctx.done></ctx.done> 上的 goroutine。不调用它会导致 goroutine 泄漏和资源残留。
常见错误是只在成功路径调用 cancel(),而 panic 或提前 return 时漏掉:
ctx, cancel := context.WithTimeout(parent, time.Second) defer cancel() // ✅ 正确:用 defer 保证执行 // ... 使用 ctx
注意:cancel() 可被多次调用,是安全的;但不能对同一个 ctx 多次 defer 同一个 cancel(比如嵌套 defer),否则可能 cancel 掉上层上下文。
为什么不能把 context 存到 struct 里长期持有?
context 是一次性的、不可重用的,设计初衷就是随请求生命周期流转。存成 struct 字段容易导致:
- 意外复用已 cancel 的 context,后续
ctx.Err()立即返回context.Canceled - 阻塞在
ctx.Done()的 goroutine 永远无法退出(如果原 context 已超时或取消) - 内存泄漏:context 携带的 value 和 deadline 会持续占用,尤其当 value 是大对象时
正确做法是每次函数调用都显式传入 fresh context:
func doWork(ctx context.Context, req *Request) error {
// ✅ 每次调用都接收新 ctx
subCtx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
defer cancel()
return callAPI(subCtx, req)
}
context.WithValue 的 key 为什么推荐用 unexported 类型?
避免不同包之间 key 冲突。如果用 string 或 int 作 key,两个无关模块都用了 "user_id",context.Value() 就会取错值。
标准做法是定义私有类型 + 变量:
type ctxKey string const userIDKey ctxKey = "user_id" ctx := context.WithValue(parent, userIDKey, 123) uid := ctx.Value(userIDKey).(int) // ✅ 类型安全,且不会和其他包冲突
另外注意:WithValue 不是为传参数设计的,只适合传递跨层的、与请求强相关且非核心业务逻辑的元数据(如 traceID、userID、requestID)。业务参数请走函数参数。
select { case
可以,但必须检查 ctx.Err() 具体是什么——因为 ctx.Done() 关闭后,ctx.Err() 可能是 context.DeadlineExceeded、context.Canceled 或 nil(极少见,仅当手动 close(done) 但没设 err)。
典型误写:
select {
case
<p>应改为:</p>
<pre class="brush:php;toolbar:false;">select {
case
<p>如果需要包装错误,也得保留原始 err:</p>
<pre class="brush:php;toolbar:false;">return fmt.Errorf("fetch data failed: %w", ctx.Err())
context 最难的部分不是 API 记忆,而是判断“这个操作到底该不该受父 context 控制”——比如后台定时任务、数据库连接池初始化、日志 flush goroutine,这些通常不该继承 request-scoped context。一不小心,整个服务就因某个慢请求被集体 cancel 掉。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











