如何清理无限写入通道的Goroutine及相关通道资源?
问题分析与解决方案
死锁原因
你的代码触发死锁的核心原因是:
- 子goroutine的循环里使用了
default分支,导致它会持续尝试往ch中写入数据。当主goroutine停止读取ch后,子goroutine会永久阻塞在ch <- 1,完全没有机会去监听done通道的信号。 - 主goroutine执行
done <- true时,由于done是无缓冲通道,且没有任何goroutine在接收它,主goroutine也会进入阻塞状态。最终所有goroutine都陷入休眠,触发死锁。
解决方案一:重构Select逻辑
把ch <- 1从default分支移到select的独立case中,让子goroutine在尝试发送数据的同时,能响应取消信号:
func Foo() (<-chan int, chan<- bool) { ch := make(chan int) done := make(chan bool) go func() { defer close(ch) for { select { case <-done: return // 收到取消信号,退出goroutine case ch <- 1: // 数据发送成功,继续循环 } } }() return ch, done } func main() { ch, done := Foo() <-ch <-ch // 发送取消信号,此时子goroutine会从select中响应done信号 done <- true // 关闭done通道(可选,避免资源泄漏) close(done) // 这里执行IO操作,比如: // resp, err := http.Get("http://google.com") }
如果担心主goroutine发送done时阻塞,可以把done声明为带缓冲的通道(容量1):done := make(chan bool, 1),这样主goroutine发送信号时不会等待子goroutine接收。
解决方案二:使用Context(Go标准库推荐)
Go的context包是处理goroutine取消的标准方案,尤其适合复杂场景下的goroutine生命周期管理:
import "context" func Foo(ctx context.Context) <-chan int { ch := make(chan int) go func() { defer close(ch) for { select { case <-ctx.Done(): return // 收到取消信号,退出 case ch <- 1: } } }() return ch } func main() { // 创建可取消的context ctx, cancel := context.WithCancel(context.Background()) defer cancel() // 确保程序退出时自动取消,避免资源泄漏 ch := Foo(ctx) <-ch <-ch cancel() // 发送取消信号 // 执行IO操作 // resp, err := http.Get("http://google.com") }
这种方式不需要手动管理done通道,context会自动传递取消信号,代码更简洁且符合Go的设计哲学。
额外注意事项
- 永远在goroutine退出时关闭通道,避免潜在的资源泄漏。
- 避免在循环中使用
default分支强制抢占select,这会导致goroutine无限制占用CPU,同时无法及时响应取消信号。
内容的提问来源于stack exchange,提问作者Sawel
相关产品推荐
相关产品推荐

