测试 channel 阻塞必须加超时,否则测试会卡死;Go 测试中直接对 channel 操作若无超时机制,会导致 goroutine 永久阻塞,测试无法结束。

测试 channel 阻塞必须加超时,否则测试会卡死
Go 测试中直接对 ch 或 <code> 做断言,一旦 channel 没有匹配的收/发端,就会永久阻塞,测试进程 hang 住,最终被 go test 强制终止(显示 “all goroutines are asleep”)。这不是偶发问题,而是无缓冲 channel 的固有行为。
正确做法是用 select + time.After 包裹操作,强制设置等待上限:
select {
case ch <- "data":
// 发送成功
case <-time.After(100 * time.Millisecond):
t.Fatal("expected send to block, but it didn't")
}- 非缓冲 channel 测试阻塞:启动 goroutine 延迟接收,主 goroutine 立即发送,再用超时判断是否真阻塞
- 缓冲 channel 测试“不阻塞”:往未满缓冲 channel 发送,应立刻返回;往已满 channel 发送,才应超时
- 避免用
time.Sleep替代超时 —— 它无法区分“慢”和“卡死”,且降低测试稳定性
验证 channel 关闭行为要检查 ok 标志,不能只看值
关闭后的 channel 仍可读,但返回的是零值 + false。如果只检查读出的值(比如 if v == 0),会把正常写入的零值误判为“已关闭”,造成误报。
必须用双值接收语法判断关闭状态:
立即学习“go语言免费学习笔记(深入)”;
v, ok := <-ch
if !ok {
t.Log("channel closed")
return
}
if v != expected {
t.Errorf("got %v, want %v", v, expected)
}-
close(ch)后继续ch 会 panic:<code>panic: send on closed channel,测试中需 recover 或隔离执行 - 重复
close(ch)同样 panic:panic: close of closed channel,多 goroutine 场景建议用sync.Once包裹 - nil channel 上执行
close(nilCh)也 panic:panic: close of nil channel,初始化检查不可省
for range 退出时机错位是常见测试失焦点
for range ch 不会在 channel 关闭“瞬间”退出 —— 它只在尝试从已关闭且**缓冲区为空**的 channel 读取时才终止。如果关闭前还有数据在缓冲区,循环会先把它们全吐出来,再退出。
这意味着:若测试期望 “close 后 for range 立即结束”,但实际缓冲里还有残留数据,就会卡在下一次迭代,导致超时或误判。
- 测试前确保缓冲 channel 已清空,或改用
v, ok := 循环手动控制退出逻辑 - 多个 sender 场景下,
close(ch)必须发生在**最后一个 sender 完成发送之后**,否则部分数据丢失 - receiver 启动早于 sender 时,
for range会一直阻塞等第一个值 —— 这不是 bug,是设计使然,测试需覆盖该等待期
容易被忽略的底层细节:nil channel 和 recvq/sendq 状态
测试中常以为 “没 make 就是空 channel”,但 var ch chan int 是 nil,它和已关闭 channel 表现不同:nil channel 的任何收发操作都永久阻塞(无法被 select 捕获,除非配合 default);而关闭 channel 的接收操作能立即返回零值+false。
- 用
select判断nilchannel:只有加上default才能避免阻塞,例如select { case - 缓冲区满时,sender 会进入
sendq等待;缓冲区空时,receiver 进入recvq等待 —— 这些队列状态无法直接观测,只能通过超时+行为推断 - 测试并发关闭时,不要依赖
recover捕获 panic 后继续执行 —— runtime 可能已处于不一致状态,应让测试失败并定位根源


















