必须基于上游ctx创建子context,因为context.withcancel等函数依赖父ctx的取消链路传递信号;若直接用context.background()或context.todo(),则子goroutine彻底脱离取消树,上游cancel后无法通知,导致goroutine泄漏。

context.WithCancel() 为什么必须基于上游 ctx 创建子 context
直接用 context.Background() 或 context.TODO() 启动新 goroutine,等于切断取消信号链——上游调用方 cancel 了,你的子任务完全收不到通知。
常见错误是:在 HTTP handler 里收到 ctx,却在启动子 goroutine 时写成 go doWork(context.Background());或者用 context.WithValue(ctx, key, val) 但没意识到它只是包装,不改变取消能力,前提是 ctx 本身可取消。
- 子 context 必须由
context.WithCancel(parent)、context.WithTimeout(parent, ...)等函数派生,且parent是上游传入的可取消 context - 检查是否意外“重置”了 context:比如函数签名接收
ctx context.Context,内部却调用context.WithCancel(context.Background()) - 调试技巧:打印
fmt.Printf("parent: %p, child: %p, err: %v", &parent, &child, child.Err()),确认父子地址不同但Err()变化同步
select
只表示 channel 关闭,不代表你该退出——它可能是被 <code>context.Canceled 主动终止,也可能是 context.DeadlineExceeded 超时,甚至可能是父级 context 被 cancel 导致的间接关闭。只收消息不判错,容易忽略重试、日志或资源释放逻辑的差异。
典型坑点:HTTP client 调用返回 context.Canceled,但业务层误当成网络错误重试;或数据库连接池清理时,因没区分错误类型,漏关连接。
- 永远写成
case ,而不是 <code>case - 需要差异化处理时,用
errors.Is(ctx.Err(), context.Canceled)或errors.Is(ctx.Err(), context.DeadlineExceeded) - 不要把清理逻辑全塞进
defer:goroutine 若因 panic 提前退出,defer不执行;关键资源(如文件句柄、DB 连接)应在case 分支内显式关闭
errgroup.Group 里每个 goroutine 都要自己监听 ctx
errgroup.WithContext(ctx) 只是给 group 设置默认 context,它不会自动注入到你传给 group.Go() 的函数里——那个函数如果没显式接收 ctx 并监听 ctx.Done(),就根本不会响应取消。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
现象:调用 group.Wait() 卡死,日志显示某个任务已超时,但其余 goroutine 仍在跑;或者并发请求下游服务,一个失败后其他请求还在等响应。
- 每个
group.Go(func() error { ... })函数体内,第一件事应是select监听ctx.Done(),尤其在阻塞操作(如http.Do、db.Query、time.Sleep)前 - HTTP 请求务必用
http.NewRequestWithContext(ctx, ...)构造 request,再传给client.Do();避免仅靠client.Timeout,它不参与 context 树传播 - 自定义轮询或长任务,循环开头加
if ctx.Err() != nil { return ctx.Err() },不能只在循环外 check 一次
cancel() 函数只能调用一次,且必须调用
重复调用 cancel() 会导致子 context 的 Done() channel 提前关闭,ctx.Err() 返回 context.Canceled,但此时父 context 可能还没 cancel——信号错乱,下游判断失真。
更危险的是不调用:比如写成 ctx, _ := context.WithTimeout(...),丢弃 cancel;或 defer cancel() 被提前 return 绕过。结果是底层 time.Timer 和 goroutine 持续运行,内存泄漏,CPU 空转。
- 始终用
ctx, cancel := context.WithTimeout(...),并在作用域末尾defer cancel() - 有多个出口(如 if/else、error return)时,确保每个分支都调用
cancel(),或用defer包裹在函数入口 - 绝不要用
sync.Once包裹cancel()来“防重入”——这掩盖了设计缺陷:cancel 应只在明确生命周期终点触发,比如 handler 返回、任务完成、超时到达
真正难的不是调用 cancel,而是让每个 goroutine 在任意执行点都能感知并响应它——这要求你在每个可能阻塞的地方主动插入 select,而不是依赖某一层自动转发。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










