不能在函数参数里随便加 context.Context,因为 context 必须从调用链上游显式传递,中途漏传或自行创建(如 context.Background())会导致超时、取消失效;正确做法是首参固定为上游传入的 context,并用 WithTimeout/WithValue/WithCancel 衍生,goroutine 中需显式传入且不可闭包捕获。

为什么不能在函数参数里随便加 context.Context
因为 Go 的 context 不是“传进去就能用”,它必须从调用链最上游(比如 HTTP handler 或 CLI 入口)开始显式传递,中途任何一层漏掉或用错,下游的超时、取消、值注入就全失效。常见错误是:某层函数自己 new 一个 context.Background() 或 context.TODO() 传给下一层,结果 cancel 信号根本收不到。
正确做法是:所有可能参与取消链路的函数,第一个参数必须是 context.Context,且只用上游传进来的那个,不自行创建(除非明确要截断继承)。
- HTTP handler 中用
r.Context()获取初始 context,而不是context.Background() - 数据库查询、HTTP client 调用、定时任务启动等,都必须接收并透传该 context
- 不要为了“省事”把 context 存到全局变量或 struct 字段里——它不是线程安全的共享状态,而是单次请求的控制流载体
如何在中间层做 context 衍生(timeout / value / cancel)
多层调用中常需要在某一层加超时、塞请求 ID、或主动触发取消。这时要用 context.WithTimeout、context.WithValue、context.WithCancel 基于上游 context 衍生新 context,而不是覆盖原 context。
示例:服务 A 调用 B,B 内部调用 C;B 层想限制 C 的执行时间不超过 500ms:
立即学习“go语言免费学习笔记(深入)”;
// B 层函数
func doInB(ctx context.Context) error {
// 衍生带 timeout 的子 context,父 ctx 取消时它也自动取消
ctx, cancel := context.WithTimeout(ctx, 500*time.Millisecond)
defer cancel() // 必须 defer,否则泄漏
return doInC(ctx) // 传给下一层
}
-
context.WithCancel返回的cancel函数必须调用,否则 goroutine 泄漏(尤其在循环或条件分支里容易漏) -
context.WithValue只适合传请求范围的元数据(如requestID),别传业务结构体或函数——它没有类型安全,且滥用会掩盖依赖关系 - 衍生出的 context 生命周期 ≤ 上游 context,不可反向延长
goroutine 启动时怎么安全传 context
在函数内启 goroutine 时,如果直接闭包捕获外层 context 变量,可能因变量被修改导致传入错误 context;更危险的是:外层函数返回后,goroutine 还在用已失效的 context。
正确写法是把 context 显式作为参数传入 goroutine 函数,并确保它来自上游、未被意外覆盖:
go func(ctx context.Context) {
select {
case <-ctx.Done():
log.Println("canceled:", ctx.Err())
return
default:
// work...
}
}(ctx) // 明确传入,不闭包引用
- 避免写
go func() { ... }(ctx)—— 看似一样,但若外层 ctx 是循环变量或被重赋值,就会出问题 - 所有异步操作(如
time.AfterFunc、http.Client.Do、sql.DB.QueryRowContext)都优先选带 Context 的版本 - 不要在 goroutine 里用
context.Background()替代传入的 context,那等于放弃整个链路控制
哪些地方最容易丢 context 或传错
真正踩坑的往往不是主干逻辑,而是边缘路径:错误处理分支、重试逻辑、日志封装、中间件装饰器、甚至 defer 清理函数。
- 重试时:每次重试都要用原始 context,别用上一次衍化的(比如带 timeout 的子 context 已过期)
- defer 里调用带 context 的清理函数(如
tx.RollbackContext(ctx)),要确保 ctx 在 defer 时仍有效——不能 defer 一个闭包去读局部重定义的 ctx 变量 - 中间件如 Gin 的
c.Next()后续 handler 用的是c.Request.Context(),但如果你在中间件里做了context.WithValue却没写回 request,下游拿不到 - 第三方库不支持 context?要么换库,要么用
context.WithTimeout包一层并手动检查ctx.Done()做中断(例如老版net.Conn操作)
最隐蔽的问题是:代码看起来每层都传了 context,但某层悄悄用 context.Background() 初始化了一个子任务,这个子任务就彻底脱离了请求生命周期管理——查起来得靠 pprof + trace 定位 goroutine 长时间存活。


















