
本文详解 go 中因未关闭 channel 和错误使用 sync.waitgroup 导致“all goroutines are asleep - deadlock”错误的根本原因,并提供可复用的并发控制模板。
在 Go 并发编程中,all goroutines are asleep - deadlock 是一个典型且易被忽视的运行时错误。它并非语法或逻辑错误,而是程序陷入永久阻塞状态:所有 goroutine 都在等待(如从 channel 读取、写入或等待 WaitGroup),却无人唤醒或终止,导致主 goroutine 无法继续执行而崩溃。
在你提供的代码中,问题根源有两点:
sync.WaitGroup 计数不匹配:wg.Add(len(urls)) 错误地为每个 URL 增加计数,但实际只启动了 1 个 Poller goroutine(而非 len(urls) 个)。WaitGroup 的 Add() 应与 Done() 调用次数严格对应——即每个 goroutine 生命周期 对应一次 Add(1) + Done()。当前代码中,Poller 函数内仅调用一次 wg.Done(),但 main 中却 Add(len(urls)),造成计数严重失衡,wg.Wait() 永远不会返回。
channel 未关闭导致无限阻塞:Poller 使用 for r := range in 循环持续接收数据。该循环仅在 channel 被显式关闭后才会退出。而你的写入 goroutine 在发送完所有 URL 后直接结束,既未关闭 pending channel,也未调用 wg.Done() 标记自身完成,导致 Poller 永远卡在 range 等待下一条数据,主 goroutine 则在 wg.Wait() 处死锁。
✅ 正确做法是:
- wg.Add(n) 中的 n 表示需等待的 goroutine 数量(此处为 2:1 个 Poller + 1 个发送器);
- 发送器 goroutine 完成后,先关闭 channel,再调用 wg.Done()(顺序很重要:关闭必须在所有发送操作之后,且 Done() 标识自身退出);
- Poller 中使用 defer wg.Done() 确保 goroutine 结束时正确减计数。
以下是修复后的完整可运行示例(已适配 numPollers = 2 的并发模型):
package main
import (
"fmt"
"sync"
"time"
)
const numPollers = 2 // 启动 2 个并发 Poller
var urls = []string{
"http://www.google.com/",
"http://golang.org/",
"http://blog.golang.org/",
"http://golangtutorials.blogspot.fr",
"https://gobyexample.com/",
}
type Resource struct {
url string
}
// Poller 从 channel 拉取 *Resource 并处理(此处仅打印)
func Poller(in <-chan *Resource, wg *sync.WaitGroup) {
defer wg.Done()
for r := range in { // range 会自动在 channel 关闭后退出
fmt.Printf("Processed: %s - %s\n", r.url, time.Now().Format("15:04:05"))
}
}
func main() {
var wg sync.WaitGroup
pending := make(chan *Resource, len(urls)) // 可选:加缓冲避免发送阻塞
// 启动 numPollers 个 Poller goroutine
wg.Add(numPollers)
for i := 0; i < numPollers; i++ {
go Poller(pending, &wg)
}
// 启动 1 个发送 goroutine:写入 URL 并关闭 channel
wg.Add(1)
go func() {
defer close(pending) // ✅ 关键:发送完成后关闭 channel
defer wg.Done() // ✅ 标记发送器 goroutine 完成
for _, url := range urls {
fmt.Printf("Sending: %s\n", url)
pending <- &Resource{url: url}
}
}()
wg.Wait() // 等待所有 Poller 和发送器完成
fmt.Printf("✅ All done at %s\n", time.Now().Format("15:04:05"))
}? 关键注意事项:
- close(pending) 必须由唯一写入者(即发送 goroutine)调用,且只能调用一次;多个 goroutine 写入时需额外协调。
- 若需限制并发请求数(如 HTTP 调用),应在 Poller 内部实现(例如用 http.Client 发起请求),而非依赖 channel 缓冲区大小。
- 使用 make(chan *Resource, N) 设置缓冲区可避免发送端阻塞,但不解决死锁本质——channel 关闭仍是 range 退出的必要条件。
- defer 语句按后进先出(LIFO)执行,因此 close(pending) 会在 wg.Done() 之前执行,确保 Poller 能收到关闭信号。
掌握 channel 生命周期与 WaitGroup 计数的精确匹配,是写出健壮 Go 并发程序的基石。










