
本文详解 go 中因闭包变量捕获不当和 channel 未持续监听导致的死锁问题,并提供可运行的修复方案与最佳实践。
本文详解 go 中因闭包变量捕获不当和 channel 未持续监听导致的死锁问题,并提供可运行的修复方案与最佳实践。
在 Go 并发编程中,一个常见却隐蔽的陷阱是:在 for 循环中直接启动 goroutine 并引用循环变量,同时未对 channel 进行持续读取——这两者叠加极易引发 fatal error: all goroutines are asleep - deadlock!。
? 根本原因剖析
原始代码存在两个关键错误:
循环变量 i 的闭包捕获问题
在匿名 goroutine 中直接使用 i(未传参),由于所有 goroutine 共享同一个变量 i,而循环结束后 i 已变为 2,因此 chan_array[i] 实际访问的是越界索引 chan_array[2](数组长度仅 2,有效索引为 0 和 1),导致 panic 或不可预期行为(如原示例中误写为 chan_array[i-1],仍属逻辑错误)。goroutine 过早退出,channel 写入阻塞
每个 goroutine 仅执行一次 select 读取,处理完一个值(如 1 或 2)后即退出。当后续向 chan1 和 chan2 发送 0 时,已无 goroutine 在监听——channel 无接收者,发送操作永久阻塞,主 goroutine 卡住,最终触发死锁。
✅ 正确写法:传参 + 持续监听
以下是修复后的完整、可运行代码:
package main
import "fmt"
func main() {
chan1 := make(chan int)
chan2 := make(chan int)
go routine(chan1)
go routine(chan2)
// 发送数据(顺序无关,goroutine 并发处理)
chan1 <- 1
chan2 <- 2
chan1 <- 0 // 触发第一个 goroutine 退出
chan2 <- 0 // 触发第二个 goroutine 退出
}
func routine(ch chan int) {
for { // 关键:使用 for 循环持续监听
select {
case x := <-ch:
if x == 0 {
fmt.Printf("goroutine on channel %p exiting\n", ch)
return // 退出当前 goroutine
}
fmt.Println(x)
}
}
}输出:
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
1 2 goroutine on channel 0xc000010030 exiting goroutine on channel 0xc000010038 exiting
? 注意:for { select { ... } } 是监听 channel 的标准模式;若需优雅关闭,可配合 context 或额外 done channel,但本例中 0 作为哨兵值已足够。
?️ 若坚持使用循环启动多个 goroutine(如动态数量场景)
请务必通过函数参数显式传递当前索引或 channel:
func main() {
channels := [2]chan int{
make(chan int),
make(chan int),
}
for i := range channels {
// ✅ 正确:将 i 作为参数传入闭包
go func(idx int) {
ch := channels[idx]
for {
select {
case x := <-ch:
if x == 0 {
return
}
fmt.Printf("ch[%d]: %d\n", idx, x)
}
}
}(i) // 立即调用,绑定当前 i 值
}
channels[0] <- 1
channels[1] <- 2
channels[0] <- 0
channels[1] <- 0
}⚠️ 关键注意事项总结
- 永远不要在 goroutine 中直接引用外部循环变量 i —— 必须通过参数传入或使用 let 风格(Go 1.22+ 支持 for i := range ... { go func(i int) {...}(i) })。
- 单次 select 读取无法应对多次写入 —— 若 channel 会接收多个值,请用 for-select 循环,而非一次性消费。
- 确保发送前至少有一个 goroutine 处于接收态 —— 可借助 sync.WaitGroup 等同步机制协调生命周期,避免“发送无人收”。
- 优先封装 goroutine 逻辑为独立函数(如 routine(ch chan int)),提升可读性、可测试性与复用性。
遵循以上原则,即可彻底规避此类死锁,写出健壮、清晰的 Go 并发代码。

















