runtime: exclude main goroutine blocked on select{} from goroutine leak profile

The main goroutine is no longer included
in the goroutine leak profile if blocked
at select without cases.
The main goroutine is still treated as a
leak during the analysis to avoid degrading
analysis precision (see test), but has its status changed
from leaked back to waiting before the profile is written.

Based on feedback in https://github.com/golang/go/issues/74609#issuecomment-4297980817

Change-Id: I0fa2a93227006d99c2872c503a1eb67ad606a034
GitHub-Last-Rev: 84458d2e63
GitHub-Pull-Request: golang/go#78922
Reviewed-on: https://go-review.googlesource.com/c/go/+/770020
Reviewed-by: Keith Randall <khr@google.com>
Reviewed-by: Cherry Mui <cherryyz@google.com>
Auto-Submit: Keith Randall <khr@google.com>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
This commit is contained in:
Vlad Saioc 2026-05-01 13:37:07 +00:00 committed by Gopher Robot
parent 19f8047c26
commit d81ba6c35d
3 changed files with 62 additions and 0 deletions

View file

@ -194,6 +194,12 @@ func TestGoroutineLeakProfile(t *testing.T) {
`Mixed\.func1.1\(.* \[chan send\]`,
),
makeTest("NoLeakGlobal"),
// If the main goroutine is waiting on select{}, it should not be reported as a leak.
makeTest("SelectNoCasesMain",
// However, any goroutine leaking on a channel that is reachable from the main
// goroutine should be reported.
`SelectNoCasesMain\.func1\(.* \[chan receive\]`,
),
}
// Stress tests are flaky and we do not strictly care about their output.

View file

@ -1315,6 +1315,40 @@ func findGoroutineLeaks() bool {
}
}
}
// Do not report the main goroutine if it is waiting on select{}.
//
// NOTE: We still treat the main goroutine as leaked during the analysis,
// but revert its status to _Gwaiting after the analysis to not include
// it in the goroutine leak profile.
// This preserves the effectiveness of goroutine leak detection
// if the main goroutine holds references to concurrency primitives causing
// other leaks.
//
// Example:
//
// ```go
// func main() {
// ch := make(chan int)
// go func() {
// ...
// <-ch // Leaks
// }()
//
// select {}
// }
// ```
//
// The main goroutine is blocked by select{}, but holds a reference to "ch".
// Not treating the main goroutine as leaked would cause the analysis to
// miss the legitimate leak at the child goroutine.
//
// The main goroutine should always be allgs[0], but double check
// in case that invariant changes in the future.
if gp0 := allgs[0]; gp0.goid == 1 && gp0.waitreason == waitReasonSelectNoCases {
casgstatus(gp0, _Gleaked, _Gwaiting)
}
// Put the remaining roots as ready for marking and drain them.
work.markrootJobs.Add(int32(work.nStackRoots - work.nMaybeRunnableStackRoots))
work.nMaybeRunnableStackRoots = work.nStackRoots

View file

@ -23,6 +23,7 @@ func init() {
register("NilRecv", NilRecv)
register("NilSend", NilSend)
register("SelectNoCases", SelectNoCases)
register("SelectNoCasesMain", SelectNoCasesMain)
register("ChanRecv", ChanRecv)
register("ChanSend", ChanSend)
register("Select", Select)
@ -90,6 +91,27 @@ func SelectNoCases() {
prof.WriteTo(os.Stdout, 2)
}
func SelectNoCasesMain() {
prof := pprof.Lookup("goroutineleak")
ch := make(chan int)
go func() {
// Should be reported as a leak.
<-ch
}()
go func() {
for i := 0; i < yieldCount; i++ {
runtime.Gosched()
}
// Write a goroutine leak profile from
// the child goroutine.
prof.WriteTo(os.Stdout, 2)
// Forcefully exit the program.
os.Exit(0)
}()
// Should not be reported as a leak.
select {}
}
func ChanSend() {
prof := pprof.Lookup("goroutineleak")
go func() {