goroutine泄漏比性能差更致命,需监控NumGoroutine持续上升;sync.Pool用于复用高频小对象以减GC压力;context.WithTimeout+select是并发控制底线;优先用atomic或Mutex而非channel做同步。

goroutine 泄漏比性能差更致命
并发不等于高性能,盲目加 go 关键字反而容易拖垮程序。真正影响性能的往往不是 CPU 跑得慢,而是 goroutine 积压、channel 阻塞、锁竞争或内存持续增长。观察 runtime.NumGoroutine() 在压测中是否持续上升,是判断泄漏的第一信号。
常见诱因包括:
- 向已关闭的
chan发送数据(触发 panic 或阻塞) - 无缓冲 channel 的发送方未配对接收,或接收方提前退出
- HTTP handler 中启 goroutine 但没做 context 取消传播,请求中断后协程仍在跑
- for-select 循环里忘了加
default或超时,导致协程卡死等待
用 sync.Pool 减少高频小对象 GC 压力
当程序频繁创建短生命周期结构体(如 JSON 解析中的 map[string]interface{}、网络包头缓存),GC 会成为瓶颈。这时 sync.Pool 不是“锦上添花”,而是刚需。
关键点:
- Pool 中的对象可能被任意时间回收,不能存放带状态或需显式清理的资源(如文件句柄、DB 连接)
- 初始化函数
New只在 Get 返回 nil 时调用,别在里面做重操作 - Put 前确保对象字段已清零,否则残留数据可能污染下次使用
示例:复用 bytes.Buffer
var bufPool = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
func process(data []byte) {
b := bufPool.Get().(*bytes.Buffer)
b.Reset() // 必须重置
b.Write(data)
// ... use b
bufPool.Put(b)
}
context.WithTimeout + select 是并发控制的底线
所有外部依赖调用(HTTP、DB、RPC)必须绑定 context,否则一个慢接口就能让整个 goroutine 池卡死。尤其注意 http.Client 默认不读取 context,要显式传入:
- 用
http.NewRequestWithContext(ctx, ...)替代http.NewRequest - 自定义
http.Client时设置Timeout仅作用于连接+首字节,不覆盖整个请求周期 - select 中混用 channel 接收和
ctx.Done(),避免 Goroutine 等待永远不来的响应
错误写法:resp, err := http.DefaultClient.Do(req) —— 完全忽略上下文取消
不要用 channel 做同步,优先选 sync.Mutex 或 atomic
看到 “多个 goroutine 写同一变量”,第一反应不该是“建个 channel 来协调”,而是问:这个共享状态真的需要 goroutine 间通信吗?
- 计数器类场景,
atomic.AddInt64比 channel 快 10 倍以上,且无调度开销 - 简单标志位(如 isClosed),
atomic.Load/StoreUint32足够,不用 channel 传递 bool - 只有当逻辑天然具备“生产-消费”流式特征(如日志批量刷盘、任务分发)才用 channel
滥用 channel 同步还会掩盖真实竞争:Go race detector 对 channel 操作不敏感,但对 sync.Mutex 和 atomic 覆盖完整。
并发性能优化真正的难点,不在怎么开 goroutine,而在怎么安全、及时地关掉它们;不在堆多少资源,而在清楚每一块内存何时被谁引用、何时该释放。