select {} 是唯一无副作用的永久阻塞方式,用于主 goroutine 等待;切勿在 testxxx 中使用,否则 go test 会卡死;无缓冲 channel 可复现死锁。

并发测试中 goroutine 阻塞不是目标,而是信号——它往往意味着你漏掉了同步、超时或 channel 收发匹配,而不是“想让它卡住”。真要模拟阻塞,得明确是为测超时、测死锁、还是测资源竞争。
用 select {} 模拟永久阻塞(最轻量)
这是唯一真正“无副作用”的永久阻塞方式,常用于主 goroutine 等待后台服务运行:
-
select {}不消耗 CPU,不占栈,也不触发调度器检查,Go 运行时会把它标记为 “waiting forever” - 别在测试函数里直接写
select {}—— 它会让go test卡死不动,无法退出;只应在实际程序的main()或集成测试的独立进程里用 - 如果你在
TestXXX里写了它,go test会一直 hang,连^C都可能收不到,必须 kill -9
用无缓冲 channel 模拟同步阻塞(测死锁最直接)
这是复现 “fatal error: all goroutines are asleep - deadlock!” 的标准姿势:
- 声明
ch := make(chan int)(无缓冲),然后只ch 不接收,或只 <code> 不发送,goroutine 就会卡在 channel 操作上 - 在并发测试中,故意让两个 goroutine 分别卡在 send 和 recv,就能 100% 触发死锁 panic,验证你的死锁检测逻辑是否生效
- 注意:带缓冲的 channel(如
make(chan int, 1))不会立即阻塞,容易误判;测试死锁必须用无缓冲
用 time.Sleep 模拟可控阻塞(仅限调试,禁用于正式测试)
它看起来像阻塞,但本质是“假装忙”,实际不可靠:
-
time.Sleep(5 * time.Second)不能替代sync.WaitGroup或context.WithTimeout,因为无法响应取消、无法感知 goroutine 是否真完成了工作 - 常见错误:在测试里起 goroutine 后只加
time.Sleep就结束函数,导致部分请求根本没发出就被测试框架回收 - 若真要用,必须配合
runtime.NumGoroutine()断言确认 goroutine 数回落,否则只是“看起来过了”
为什么 sync.WaitGroup.Wait() 不是阻塞模拟,而是正确做法
很多人以为 wg.Wait() 是“让 goroutine 阻塞”,其实它是“等待完成”的同步原语,和阻塞模拟无关:
- 它只在计数器为 0 时返回,否则挂起当前 goroutine —— 这是受控等待,不是 bug
- 测试中漏掉
wg.Add(1)或闭包捕获了错误的wg变量(比如传值而非传指针),才会导致wg.Wait()永久卡住,那属于 bug,不是模拟 - 真正该检查的是:所有
go func() { defer wg.Done(); ... }()是否都执行到了Done(),尤其注意 panic 路径是否遗漏defer
真正难处理的不是“怎么卡住”,而是“卡住后怎么知道为什么卡”——死锁 panic 日志里那一堆 goroutine 堆栈,比任何模拟技巧都重要。别急着写阻塞逻辑,先跑一遍 go run -race 和 go tool pprof 看看 goroutine 状态。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











