Go 语言 channel 在什么情况下会引起资源泄漏?
原创
go
go
Go语言教程
2026年6月25日 13:21
陕西
在小说阅读器读本章
去阅读
线上 goroutine 从 180 涨到 64000,CPU 没爆,内存也没立刻爆,接口就是越来越慢。
pprof 一抓,堆栈很扎眼:
goroutine 58231 [chan send]:
order/internal/worker.pushResult(...)
/app/worker/checker.go:47
goroutine 58232 [chan receive]:
order/internal/worker.start(...)
/app/worker/checker.go:83
这种我第一眼一般不先看 GC,也不先怀疑数据库。Go 里看到 [chan send]、[chan receive] 长时间堆着,八成是 channel 把 goroutine 挂死了。
channel 本身不是洪水猛兽,真正泄漏的通常是 goroutine。
一个 goroutine 卡在 channel 上,只要它不退出,它手里引用的对象、上下文、连接、buffer,就都可能跟着活着。表面看是 channel 问题,实际是退出路径没设计好。
最常见的是发送没人接。
比如有个订单校验任务,后台并发查几个规则,谁先返回就用谁的结果。代码一开始可能这么写:
func checkOrder(orderID string) bool {
resultCh := make(chan bool)
go func() {
ok := checkRisk(orderID)
resultCh <- ok
}()
select {
case ok := <-resultCh:
return ok
case <-time.After(200 * time.Millisecond):
return false
}
}
这段代码看着没毛病,超时返回,挺干净。
但问题在这里:一旦 checkRisk 超过 200ms,外层函数已经走了,resultCh 没人接了。后面的 resultCh <- ok 会一直卡住。
请求量一大,goroutine 就开始堆。
我一般会先加一行监控日志,不用上来就搞复杂工具:
go func() {
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
for range ticker.C {
log.Printf(\"goroutines=%d\", runtime.NumGoroutine())
}
}()
如果这个数字只涨不降,就别安慰自己“业务高峰正常”。正常高峰是会上下波动,不是一路爬坡。
这段代码改法也不复杂,关键是给发送方也留退出口:
func checkOrder(ctx context.Context, orderID string) bool {
resultCh := make(chan bool, 1)
go func() {
ok := checkRisk(orderID)
select {
case resultCh <- ok:
case <-ctx.Done():
log.Printf(\"risk result dropped, order=%s\", orderID)
}
}()
timer := time.NewTimer(200 * time.Millisecond)
defer timer.Stop()
select {
case ok := <-resultCh:
return ok
case <-timer.C:
return false
case <-ctx.Done():
return false
}
}
这里我会把 resultCh 改成缓冲为 1。不是说所有 channel 都要加 buffer,而是这种“最多只返回一个结果”的场景,外层即使超时走了,子 goroutine 也能把结果塞进去然后退出。
但别滥用 buffer。buffer 不是垃圾桶,它只是把阻塞往后推了一点。
第二种泄漏,是接收方一直等。
func consume(jobCh <-chan Job) {
for {
job := <-jobCh
handle(job)
}
}
这段代码的问题是,如果上游不再发送了,又没有关闭 channel,这个 goroutine 会永远卡在 <-jobCh。
生产里经常藏在 worker 里。服务都要停了,worker 还挂着;任务调度已经取消了,消费协程还在等。
我更愿意写成这样:
func consume(ctx context.Context, jobCh <-chan Job) {
for {
select {
case job, ok := <-jobCh:
if !ok {
log.Printf(\"job channel closed\")
return
}
handle(job)
case <-ctx.Done():
log.Printf(\"consumer stopped: %v\", ctx.Err())
return
}
}
}
注意这个 ok。channel 被关闭之后,接收方还能读到零值,不判断 ok,有时候会把空任务当真任务处理,这个坑我见过,不止一次。
第三种,是生产者开了一堆,没人负责收尸。
func dispatch(ids []string) <-chan Order {
out := make(chan Order)
for _, id := range ids {
go func(orderID string) {
order := loadOrder(orderID)
out <- order
}(id)
}
return out
}
调用方如果只读前 10 条,剩下那些 goroutine 全卡在 out <- order。
这种代码在线下测不出来,因为测试数据少,读得也完整。到了线上,分页、超时、用户取消请求一来,马上露馅。
我一般会把并发数、退出信号、关闭动作都写明白:
func dispatch(ctx context.Context, ids []string) <-chan Order {
out := make(chan Order, 32)
guard := make(chan struct{}, 8)
var wg sync.WaitGroup
for _, id := range ids {
select {
case <-ctx.Done():
break
case guard <- struct{}{}:
}
wg.Add(1)
go func(orderID string) {
defer wg.Done()
defer func() { <-guard }()
order := loadOrder(orderID)
select {
case out <- order:
case <-ctx.Done():
log.Printf(\"skip send order=%s\", orderID)
}
}(id)
}
go func() {
wg.Wait()
close(out)
}()
return out
}
这段不漂亮,但我放心。并发数量有上限,发送有退出口,所有 goroutine 结束后关闭 out。线上代码有时候就该写得笨一点,别追求那种一眼看着很高级的链式封装。
还有一个容易被忽略的点:time.After 放在循环里。
for {
select {
case item := <-ch:
handle(item)
case <-time.After(time.Second):
log.Println(\"idle\")
}
}
低频没事,高频循环里这么写,会不停创建 timer。虽然 timer 到期后会被回收,但在压力大的时候,这种临时对象会把内存和 GC 搞得很难看。
我更喜欢显式拿着 timer 或 ticker:
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for {
select {
case item, ok := <-ch:
if !ok {
return
}
handle(item)
case <-ticker.C:
log.Println(\"idle\")
case <-ctx.Done():
return
}
}
查 channel 泄漏,不要只盯代码猜。
我一般顺序是这样:
curl http://127.0.0.1:6060/debug/pprof/goroutine?debug=2 | grep -E \"chan send|chan receive\" -n
然后看是不是同一个业务栈重复出现。如果几万条都卡在同一行,别犹豫,那一行就是现场。
channel 最容易泄漏的情况就这几类:发送没人接,接收没人发,退出没监听,关闭没约定,循环里乱造 timer。
Go 不会替你决定 goroutine 什么时候该死。
这个事必须代码里写清楚。否则 channel 看着很优雅,线上挂起来也很安静,等你发现的时候,goroutine 已经堆成一堵墙了。
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。





![[译苑雅集Vol.12]高强度使用Mythos两个月后,Cloudflare打造了一套不依赖单一模型的企业级漏洞发现工程系统"](/images/random/titlepic/15.jpg)



评论