goroutine泄漏最直接信号是runtime.numgoroutine()持续单向增长且不回落,需结合/debug/pprof/goroutine?debug=2查看大量阻塞在chan receive、netpoll或semacquire的堆栈,卡住超10秒基本可判定泄漏。

goroutine 泄漏比内存泄漏更隐蔽,必须主动监控
高并发下 goroutine 泛滥是服务雪崩的常见起点。它不报错、不 panic,但 CPU 持续 100%、runtime.NumGoroutine() 持续上涨就是典型信号。
关键做法:
- 所有带超时的 I/O 操作(HTTP client、DB query、RPC 调用)必须配
context.WithTimeout或context.WithDeadline,不能只靠time.Sleep或无约束 for-select - 启动长期运行的
goroutine前,先确认它有明确退出路径——比如监听ctx.Done()并 clean up,而非靠 global flag 轮询 - 上线后定期用
curl http://localhost:6060/debug/pprof/goroutine?debug=2抓快照,对比阻塞栈中重复出现的函数调用链
sync.Pool 不是万能缓存,误用反而拖慢吞吐
sync.Pool 适合复用「临时、短生命周期、构造开销大」的对象(如 bytes.Buffer、自定义 parser 结构体),但绝不该用来替代 map 或 redis 存业务数据。
常见踩坑点:
- Pool 中对象被 GC 回收无通知,若缓存了含指针或外部资源引用的结构体(如未 Close 的
sql.Rows),会导致资源泄漏 - Pool 在 GC 时清空全部内容,高频率 Put/Get 且对象大小波动大时,可能引发频繁内存分配,压测时
pprof allocs曲线会异常凸起 - 不要在 Pool 的
New函数里做初始化以外的事(如打日志、发 metric),它可能被任意 goroutine 调用,且无并发保护
channel 容量不是越大越好,阻塞写入才是保护机制
用 make(chan T, N) 创建带缓冲 channel 时,N 取值应基于「最大可容忍积压量」,而不是“怕丢数据”就设成 10000。
在 Go 中使用 google/wire 实现编译时依赖注入——wire.NewSet、wire.Build、wire.Bind(接口→实现)、wire.Struct、wire.Value、wire.Interface
真实影响:
- 缓冲区过大 → 写入不阻塞 → 生产者持续 push → 消费者跟不上 → channel 占用大量堆内存 → GC 压力陡增
- 无缓冲 channel(
make(chan T))天然提供背压:生产者必须等消费者 ready 才能写,适合强一致性要求场景(如配置热更新通知) - 若需异步解耦又怕积压,优先考虑「带超时的非阻塞写」:
select { case ch ,比盲目扩 buffer 更可控
HTTP handler 中禁止直接传 context.Background()
每个 HTTP 请求进来的 *http.Request 都自带一个 request-scoped context.Context,它携带超时、取消、traceID 等关键信息。用 context.Background() 会切断整个链路。
后果很直接:
- 下游调用(DB、gRPC)无法继承上游 timeout,导致请求 hang 死,连接池耗尽
- OpenTelemetry / Jaeger 追踪断链,span parent ID 丢失
- 中间件注入的 value(如用户身份、region)在子 goroutine 中取不到
正确写法永远是:req.Context() 作为根 context,再派生子 context:ctx, cancel := context.WithTimeout(req.Context(), 5*time.Second)。
真正难的是边界判断:什么时候该用 WithCancel,什么时候该用 WithTimeout,以及 cancel 是否一定在 defer 中调用——这些细节不写进日志、不暴露指标,线上就只能靠 pprof + 日志交叉定位。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










