Why does Go test with blocked channel not reporting deadlock

Hi, I met a strange issue when doing test with channels.

In a normal main function, the following code will report the deadlock error.

package main

import (
	"fmt"
)

func main() {
	c := make(chan int)
	c <- 1
	fmt.Println(<-c)
}

But on my machine, this simple test seems deadlocked or blocked forever or just fails to exit for whatever reason I don’t know. I invoked the test in both Emacs and terminal, and I got the same result. The command is go test -run TestChan\$ . -v -count=1. I tried with a simpler command (go test -run TestChan) but still got the same result. I tried it on Go playground (here) and it reported the deadlock error. Is there something wrong with my Go environment?

package main

import (
	"fmt"
	"testing"
)

func TestChan(t *testing.T) {
	c := make(chan int)
	c <- 1
	fmt.Println(<-c)
}

----------------------------------------------------------------------------------------------------

Update

Looks like I haven’t made my question clear. The situation is: the same test behaves differently on my machine and on Go playground. Now I set -timeout 5s, but the error message is different from that on Go playground.
Another thing I found different with my local is that the test runner seems different from my local. It’s under package go-faketime.

local output

$ 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 output

=== 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

My questions are

  • Why does Go test with blocked channel not reporting deadlock?

  • If it’s work-as-design (because there are other goroutines running meanwhile), then how does the same test in Go playground reports the same error message as if the code is run in a main func? (this question diverges from the domain of Go channel to how Go Playground handles test)

1 Like

What happens if you remove the timeout when you run it locally?

In main() the function is run in a single goroutine. So, at start you have only 1 goroutine (except runtime). In tests, it’s not true.

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)
				}
				// Run catching the signal rather than the tRunner as a separate
				// goroutine to avoid adding a goroutine during the sequential
				// phase as this pollutes the stacktrace output when aborting.
				go func() { <-t.signal }()
			})
			ok = ok && !t.Failed()
			ran = ran || t.ran
		}
	}
	return ran, ok
}

Every test is run in a separate goroutine and there’s more code under the hood.

@bklimczak @skillian Thank you both for your replies.

Besides the fact that there are more than one goroutines when go test runs tests, another interesting thing is Go playground puts code in test into a main function and runs the main binary directly. See reply from @Jimb in my original post at stackoverflow.