
本文解析 go 语言中 goroutine 和 channel 的生命周期关系,明确说明未接收的发送操作是否导致资源泄漏,并通过缓冲通道对比阐明 gc 触发条件。
本文解析 go 语言中 goroutine 和 channel 的生命周期关系,明确说明未接收的发送操作是否导致资源泄漏,并通过缓冲通道对比阐明 gc 触发条件。
在 Go 中,goroutine 和 channel 并非“捆绑”被垃圾回收——它们的可回收性取决于是否仍存在可达引用,以及是否存在阻塞的通信行为。关键不在于“goroutine 是否关联某个 channel”,而在于 goroutine 是否仍在运行、是否被阻塞、以及其持有的资源是否还被其他活跃对象引用。
以问题中的代码为例:
func waitForOneOfTwoProcesses() {
c := make(chan bool) // 无缓冲 channel
go func() {
time.Sleep(1 * time.Second)
c <- true // 阻塞:等待接收者读取
}()
go func() {
time.Sleep(2 * time.Second)
c <- true // 同样阻塞(因仅有一个接收 `<-c`)
}()
<-c // 仅接收一次
}该函数存在资源泄漏:
- c 是无缓冲 channel,发送操作 c <- true 是同步且阻塞的;
- 第一个 goroutine 发送后立即被唤醒并退出;
- 第二个 goroutine 在 c <- true 处永久阻塞(因 <-c 已返回,channel 无人再读);
- 该 goroutine 永远无法结束,其栈、局部变量(包括对 c 的引用)持续存活;
- c 本身也被该 goroutine 持有,无法被 GC 回收。
⚠️ 注意:GC 不会主动“杀死”阻塞的 goroutine。Go 的垃圾收集器只回收不可达对象(即没有任何活跃 goroutine 或全局变量能访问到的对象)。只要 goroutine 还在运行(哪怕阻塞),它就被视为活跃,其所持资源均不可回收。
✅ 正确修复方式是使用带缓冲的 channel:
func waitForOneOfTwoProcessesFixed() {
c := make(chan bool, 1) // 缓冲大小为 1
go func() {
time.Sleep(1 * time.Second)
c <- true // 立即返回:值入缓冲队列
}()
go func() {
time.Sleep(2 * time.Second)
c <- true // 同样立即返回(缓冲未满)
}()
<-c // 接收一个值
// 函数返回后,c 被释放,两个 goroutine 均已自然退出
}当 cap(c) >= 1 时:
- 所有发送操作均为非阻塞(缓冲区有空间);
- 两个 goroutine 均能顺利完成发送并退出;
- c 在函数作用域结束后变为不可达,连同其底层数据结构一同被 GC 回收;
- 无需显式关闭 channel(除非需通知接收方“不再有数据”,但本例中无此需求)。
? 补充说明:
- 缓冲大小为 2 或更大效果相同,但 缓冲大小为 1 已足够避免泄漏;
- 若 channel 被多个 goroutine 共享或逃逸到包级变量,则需额外分析引用链;
- 使用 select + default 或 context 可主动中断等待,进一步提升健壮性;
- go vet 和 pprof 可辅助检测长期阻塞的 goroutine(如 runtime.NumGoroutine() 异常增长)。
总之:Go 不“协同回收”goroutine 和 channel;能否回收,取决于 goroutine 是否终止,而 goroutine 是否终止,取决于其是否从阻塞中解脱。合理设计 channel 缓冲与通信协议,是避免泄漏的根本之道。

















