贝利信息

Go 中使用 for range 遍历未关闭通道导致死锁的解决方案

日期:2026-01-14 00:00 / 作者:霞舞

当对未关闭的无缓冲通道使用 `for range` 时,循环会在所有数据读取完毕后持续阻塞等待新值,而发送方 goroutine 已退出且通道未关闭,最终引发“all goroutines are asleep”死锁。

在 Go 中,for v := range ch 语句具有明确的语义:它会持续接收通道 ch 中的值,直到该通道被显式关闭(close(ch))。一旦通道关闭,range 自动退出循环;若通道始终保持打开状态,即使所有发送 Goroutine 已完成并退出,range 仍会无限阻塞——因为 Go 运行时无法自动推断“不会再有新值发送”,它只依赖 close 信号作为终止依据。

你的原始代码中:

✅ 正确做法是:在确认所有发送操作完成后,由某个 Goroutine 显式关闭通道。常用模式是结合 sync.WaitGroup 等待所有发送者结束,再关闭通道:

package main

import (
    "fmt"
    "sync"
)

func sum_up(my_int int, cs chan int, wg *sync.WaitGroup) {
    defer wg.Done() // 更简洁的收尾方式
    my_sum := 0
    for i := 0; i < my_int; i++ {
        my_sum += i
    }
    cs <- my_sum
}

func main() {
    var wg sync.WaitGroup
    my_channel := make(chan int)

    // 启动发送 Goroutine,并注册 WaitGroup 计数
    for i := 2; i < 5; i++ {
        wg

.Add(1) go sum_up(i, my_channel, &wg) } // 单独 Goroutine 等待全部发送完成,然后关闭通道 go func() { wg.Wait() close(my_channel) // 关键:通知 range 可以退出 }() // range 安全退出:收到所有值 + 通道关闭信号 for ele := range my_channel { fmt.Println(ele) } fmt.Println("Done") }

⚠️ 注意事项:

总结:Go 的 for range 与通道生命周期强绑定,关闭通道是唯一合法的终止信号。理解这一设计原则,是写出健壮并发程序的关键基础。