
本文详解 go 中无缓冲通道的阻塞机制,指出 `chin
在 Go 并发编程中,通道(channel)是 Goroutine 间通信的核心原语,但其行为常被误解——尤其是“非阻塞”这一概念。你遇到的问题非常典型:程序在 chin <- vin 处卡死,即使已启动 5 个 worker(),仍无法完成数据发送循环。根本原因在于:无缓冲通道(make(chan T))的发送操作天生就是同步且阻塞的——它必须等待至少一个 Goroutine 同时执行对应的接收操作(<-chin),二者“手递手”完成数据传递后才继续执行。
你观察到“阻塞在第 5 次发送后”,恰好印证了这一点:5 个 worker() 启动后立即进入 for res := range chin,开始等待接收;但若 rows.Next() 循环在 worker() 真正开始接收前就发起第一次发送,而此时尚无接收方就绪,发送即阻塞。更关键的是,你的主 goroutine 在发送完所有数据后,才去读 chout,而 worker() 的 chout <- v 同样是无缓冲的——当所有 worker 都在尝试发送结果但主 goroutine 还未开始接收时,chout 也会阻塞,形成死锁闭环。
✅ 正确解法:三步破局
1. 使用带缓冲的通道(最直接)
为 chin 和 chout 设置合理容量,使发送端无需等待接收即可暂存数据:
var chin = make(chan in, 100) // 缓冲区容纳 100 个待处理任务 var chout = make(chan out, 100) // 缓冲区容纳 100 个处理结果
这样 chin <- vin 最多在缓冲区满时阻塞(如你实验中“阻塞在第 105 次”),而非立即阻塞。但需注意:缓冲大小应基于内存与吞吐权衡,过大易 OOM,过小仍可能阻塞。
2. 解耦发送与接收逻辑(推荐实践)
将 chout 的消费逻辑移至独立 goroutine,避免主流程串行依赖:
// 启动后台消费者,持续处理结果
go func() {
for res := range chout {
update5.Exec(res.data, res.id)
}
}()
// 主 goroutine 专注发送任务
for rows.Next() {
// ... 扫描数据
chin <- vin // 此处仍可能因 chin 无缓冲而阻塞,但 chout 已解耦
numtodo++
}
rows.Close()
// 发送完毕后关闭输入通道,通知 workers 退出
close(chin)3. 显式关闭通道 + 使用 range(健壮收尾)
仅靠 for range 读取通道是不够的——若通道永不关闭,range 将永远阻塞。务必在所有发送完成后调用 close():
// 所有任务发送完毕后
close(chin) // 告诉所有 worker:不再有新任务
// worker 内部自动退出
func worker() {
for res := range chin { // 当 chin 关闭时,循环自然结束
var v out
v.id = res.id
v.data = process(res.data)
chout <- v
}
}
// 同理,处理完所有结果后关闭输出通道
go func() {
for res := range chout {
update5.Exec(res.data, res.id)
}
close(chout) // 可选,若后续无其他接收者则非必需
}()⚠️ 关键注意事项
- 永远不要对 nil 通道或已关闭的通道执行发送操作,这会引发 panic;
- close(ch) 只能由发送方调用,且只能调用一次;
- 若使用 select 实现非阻塞发送,需配合 default 分支(但本场景中缓冲+关闭已足够);
- 数据库操作(如 rows.Scan)本身可能阻塞,确保 nextbatch2.Query() 返回的 rows 可正常迭代,避免底层连接问题被误判为通道阻塞。
通过以上改造,你的管道模型将变为:主 goroutine 快速注入任务 → 多 worker 并行处理 → 后台 goroutine 异步消费结果,彻底消除发送阻塞,实现真正高效的并发流水线。










