Go 的并发能力很强,但'能开很多协程'不等于'系统就会跑得稳'。线上真正出问题的,往往不是语法,而是 Goroutine 没退、Channel 卡住、Context 没传对。量一上来,这些小毛病就会变成内存飙升、请求堆积,最后拖垮整个服务。
这篇内容不讲概念堆砌,直接围绕几个常见坑来拆:怎么定位 Goroutine 泄漏,Channel 为什么会阻塞,Context 应该怎么传,连接池该怎么设,最后再看一组能复现问题的调试场景。顺序不复杂,基本都是线上会碰到的事。
一、Goroutine 泄漏:先看是不是没退出
Goroutine 泄漏是 Go 服务里最容易被忽略的问题之一。协程启动后一直挂着,短时间看不出毛病,时间一长,内存和调度开销都会慢慢堆上去。
常见的几个入口
- Channel 没人消费:生产者还在写,消费者已经走了;
- Context 取消了,但子协程没监听;
- for 循环没有退出条件,一直跑到服务重启。
用 net/http/pprof 先把栈抓出来
在服务里打开 pprof:
import _ "net/http/pprof"
然后访问 /debug/pprof/goroutine?debug=2,先看 goroutine 栈;如果要进一步分析,也可以走 go tool pprof:
go tool pprof http://localhost:6060/debug/pprof/goroutine
(pprof) top
(pprof) list YourLeakingFunc
我一般会先盯 goroutine 数量的走势。它应该跟负载一起上下波动,而不是一路往上爬。稳态下如果已经超过 1000,还继续涨,基本就该查了。
二、Channel 阻塞:别把同步和异步混在一起
Channel 是 goroutine 之间传递数据的通道,但它不是'放进去就完事'的队列。写法不对,阻塞和死锁都很常见。
1. 无缓冲 Channel:两边必须同时在线
ch := make(chan int) // 无缓冲
go func() {
ch <- 1
}() // 如果没有 receiver,这个 Goroutine 会一直卡住
这种写法适合明确的一对一同步场景。优点是简单,问题也很直接:收发双方只要有一边晚到,就会卡住。
2. 缓冲 Channel:只是缓一口气
ch := make(chan int, 10) // 缓冲 10
缓冲能做削峰,适合生产速度和消费速度不一致的场景。但它不是无限队列,写满之后照样阻塞。这个坑很常见,尤其是在高峰流量下,很多人会误以为'加了缓冲就安全了'。



