Go Channel 与 Goroutine 中指针变量引发的死锁问题解析

本文详解 go 中因未关闭 channel 和错误使用 sync.waitgroup 导致“all goroutines are asleep - deadlock”错误的根本原因,并提供可复用的并发控制模板。

在 Go 并发编程中,all goroutines are asleep - deadlock 是一个典型且易被忽视的运行时错误。它并非语法或逻辑错误,而是程序陷入永久阻塞状态:所有 goroutine 都在等待(如从 channel 读取、写入或等待 WaitGroup),却无人唤醒或终止,导致主 goroutine 无法继续执行而崩溃。

在你提供的代码中,问题根源有两点:

  1. 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() 永远不会返回。

  2. 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 并发程序的基石。