Golang中为什么测试时阻塞的channel不会报告死锁
Golang中为什么测试时阻塞的channel不会报告死锁 我在进行通道测试时遇到了一个奇怪的问题。
在一个普通的main函数中,以下代码会报告死锁错误。
package main
import (
"fmt"
)
func main() {
c := make(chan int)
c <- 1
fmt.Println(<-c)
}
但在我的机器上,这个简单的测试似乎死锁了,或者永远阻塞,或者由于某些我不知道的原因无法退出。我在Emacs和终端中都调用了测试,得到了相同的结果。使用的命令是 go test -run TestChan\$ . -v -count=1。我也尝试了更简单的命令(go test -run TestChan),但结果仍然相同。我在Go playground(这里)上尝试了,它报告了死锁错误。是我的Go环境有问题吗?
package main
import (
"fmt"
"testing"
)
func TestChan(t *testing.T) {
c := make(chan int)
c <- 1
fmt.Println(<-c)
}
----------------------------------------------------------------------------------------------------
更新
看起来我没有把我的问题说清楚。情况是:相同的测试在我的机器上和Go playground上的表现不同。现在我设置了 -timeout 5s,但错误信息与Go playground上的不同。
我发现的另一个与本地环境不同的地方是,测试运行器似乎与我的本地环境不同。它属于 go-faketime 包。
本地输出
$ go test main_test.go -timeout 5s
panic: test timed out after 5s
goroutine 17 [running]:
testing.(*M).startAlarm.func1()
/usr/local/go/src/testing/testing.go:1460 +0xdf
created by time.goFunc
/usr/local/go/src/time/sleep.go:168 +0x44
goroutine 1 [chan receive]:
testing.(*T).Run(0xc000108120, 0x1141975, 0x8, 0x114a528, 0x1075a96)
/usr/local/go/src/testing/testing.go:1044 +0x37e
testing.runTests.func1(0xc000108000)
/usr/local/go/src/testing/testing.go:1285 +0x78
testing.tRunner(0xc000108000, 0xc000066e10)
/usr/local/go/src/testing/testing.go:992 +0xdc
testing.runTests(0xc00000c060, 0x1236220, 0x1, 0x1, 0x0)
/usr/local/go/src/testing/testing.go:1283 +0x2a7
testing.(*M).Run(0xc000106000, 0x0)
/usr/local/go/src/testing/testing.go:1200 +0x15f
main.main()
_testmain.go:44 +0x135
goroutine 6 [chan send]:
command-line-arguments.TestChan(0xc000108120)
/Users/james/prog/allez/mtest/main_test.go:10 +0x59
testing.tRunner(0xc000108120, 0x114a528)
/usr/local/go/src/testing/testing.go:992 +0xdc
created by testing.(*T).Run
/usr/local/go/src/testing/testing.go:1043 +0x357
FAIL command-line-arguments 5.013s
FAIL
Go playground 输出
=== RUN TestChan
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive]:
testing.(*T).Run(0xc00011a120, 0x4f71c0, 0x8, 0x4ff688, 0x498336)
/usr/local/go-faketime/src/testing/testing.go:1043 +0x37e
testing.runTests.func1(0xc00011a000)
/usr/local/go-faketime/src/testing/testing.go:1284 +0x78
testing.tRunner(0xc00011a000, 0xc000066df8)
/usr/local/go-faketime/src/testing/testing.go:991 +0xdc
testing.runTests(0xc00010c040, 0xc00010c020, 0x1, 0x1, 0x0)
/usr/local/go-faketime/src/testing/testing.go:1282 +0x2a7
testing.(*M).Run(0xc000118000, 0x0)
/usr/local/go-faketime/src/testing/testing.go:1199 +0x15f
testing.Main(0x4ff690, 0xc00010c020, 0x1, 0x1, 0x0, 0x0, 0x0, 0x5e8860, 0x0, 0x0)
/usr/local/go-faketime/src/testing/testing.go:1126 +0xd4
main.main()
/tmp/sandbox970213620/prog.go:24 +0x9c
goroutine 18 [chan send]:
main.TestChan(0xc00011a120)
/tmp/sandbox970213620/prog.go:10 +0x59
testing.tRunner(0xc00011a120, 0x4ff688)
/usr/local/go-faketime/src/testing/testing.go:991 +0xdc
created by testing.(*T).Run
/usr/local/go-faketime/src/testing/testing.go:1042 +0x357
我的问题是
- 为什么使用阻塞通道的Go测试不报告死锁?
- 如果这是设计使然(因为同时有其他goroutine在运行),那么为什么Go playground中的相同测试会报告与在main函数中运行代码时相同的错误信息?(这个问题从Go通道的领域转向了Go Playground如何处理测试)
更多关于Golang中为什么测试时阻塞的channel不会报告死锁的实战教程也可以访问 https://www.itying.com/category-94-b0.html
如果在本地运行时移除超时设置,会发生什么?
更多关于Golang中为什么测试时阻塞的channel不会报告死锁的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
@bklimczak @skillian 感谢两位的回复。
除了 go test 运行测试时会启动多个 goroutine 这一事实外,另一个有趣的事情是 Go playground 会将测试代码放入一个 main 函数中并直接运行该主二进制文件。请参阅我在 stackoverflow 上的原始帖子中来自 @Jimb 的回复。
在 main() 函数中,该函数是在单个 goroutine 中运行的。因此,开始时你只有一个 goroutine(除了运行时)。但在测试中,情况并非如此。
func runTests(matchString func(pat, str string) (bool, error), tests []InternalTest) (ran, ok bool) {
ok = true
for _, procs := range cpuList {
runtime.GOMAXPROCS(procs)
for i := uint(0); i < *count; i++ {
if shouldFailFast() {
break
}
ctx := newTestContext(*parallel, newMatcher(matchString, *match, "-test.run"))
t := &T{
common: common{
signal: make(chan bool),
barrier: make(chan bool),
w: os.Stdout,
chatty: *chatty,
},
context: ctx,
}
tRunner(t, func(t *T) {
for _, test := range tests {
t.Run(test.Name, test.F)
}
// 通过捕获信号来运行,而不是将 tRunner 作为一个单独的 goroutine 运行,以避免在顺序阶段添加 goroutine,因为这会在中止时污染堆栈跟踪输出。
go func() { <-t.signal }()
})
ok = ok && !t.Failed()
ran = ran || t.ran
}
}
return ran, ok
}
每个测试都在一个单独的 goroutine 中运行,并且底层还有更多代码。
在Go测试中,阻塞的channel不报告死锁是因为测试框架会启动额外的goroutine来管理测试执行。当测试函数中的channel操作阻塞时,测试框架的goroutine仍在运行,因此Go运行时不会检测到"所有goroutine都处于休眠状态"的死锁条件。
以下是示例代码说明:
package main
import (
"testing"
"time"
)
func TestBlockingChannel(t *testing.T) {
// 无缓冲channel,发送操作会阻塞直到有接收者
ch := make(chan int)
// 这个发送操作会永久阻塞,因为没有接收者
ch <- 42
// 这行永远不会执行
t.Log("This will never be printed")
}
func TestChannelWithGoroutine(t *testing.T) {
ch := make(chan int)
// 启动一个goroutine来接收
go func() {
<-ch
}()
// 现在发送不会阻塞
ch <- 42
}
func TestChannelDeadlockInMain(t *testing.T) {
// 在单独的goroutine中运行会死锁的代码
done := make(chan bool)
go func() {
defer func() {
if r := recover(); r != nil {
t.Logf("Recovered from panic: %v", r)
}
done <- true
}()
// 这段代码在main函数中会死锁
c := make(chan int)
c <- 1
<-c
}()
select {
case <-done:
t.Log("Test completed")
case <-time.After(1 * time.Second):
t.Error("Test timed out")
}
}
测试框架的架构决定了死锁检测的行为差异。go test命令会启动一个主测试goroutine来协调所有测试的执行,即使测试函数中的channel操作被阻塞,这个主goroutine仍然在运行,等待测试超时或完成。
Go Playground使用修改过的运行时环境,包括go-faketime包,这可能会影响死锁检测的行为。在Playground环境中,测试运行器可能以不同的方式处理goroutine调度和死锁检测。
要验证测试中的死锁情况,可以使用超时机制:
func TestChannelWithTimeout(t *testing.T) {
ch := make(chan int)
select {
case ch <- 42:
t.Error("Should not reach here")
case <-time.After(100 * time.Millisecond):
t.Log("Channel send blocked as expected")
}
}
或者使用带缓冲的channel来避免阻塞:
func TestBufferedChannel(t *testing.T) {
// 带缓冲的channel不会立即阻塞
ch := make(chan int, 1)
ch <- 42 // 不会阻塞
value := <-ch
if value != 42 {
t.Errorf("Expected 42, got %d", value)
}
}
测试环境和main函数环境的goroutine调度差异导致了死锁报告行为的不同。测试框架维持着活跃的goroutine,而普通的main函数在channel阻塞后就没有其他活跃的goroutine了。

