Go

What are the scheduling implications of runtime.Goexit, blocking in init, and unbuffered sends from many goroutines?

Question 178HardGo 1.22 to 1.25
  • runtime.Goexit() ends the calling goroutine after running all its deferred calls. A recover in those deferred calls returns nil, because Goexit is not a panic. t.FailNow relies on it, which is why calling t.Fatal from a goroutine the test started is wrong: it exits that goroutine, not the test.
  • Package init runs in one goroutine before main. Goroutines started in init do run, but if init blocks waiting on something that depends on main, the program deadlocks. Keep init free of side effects and do not block in it.
  • Many senders on an unbuffered channel form a FIFO queue of sudogs in sendq. The receiver wakes them in order, one handoff at a time. That is fair, but it serializes the senders, and each wake-up costs a scheduler round trip. A buffered channel sized for the typical burst lowers wake-up overhead. It does not add throughput if the consumer is the bottleneck.
func TestX(t *testing.T) {
	errc := make(chan error, 1)
	go func() { errc <- doWork() }() // report back, don't t.Fatal here
	if err := <-errc; err != nil {
		t.Fatal(err)
	}
}

More on Goroutines & the Scheduler

All 35 Goroutines & the Scheduler questions