Detecting Goroutine Leaks with Go's Experimental goroutineleak Feature
This article explains how Go's experimental goroutineleak experiment integrates leak detection into the garbage collector, describes the underlying sudog mechanism, and provides step‑by‑step instructions to enable the feature, run a demo, and analyze leaks using pprof.
Goroutine Leak Problem
In Go, a goroutine leak occurs when a goroutine blocks on a synchronization primitive (e.g., channel, mutex) that becomes permanently unreachable. Each leaked goroutine retains at least 2 KB of stack, causing memory growth, scheduler pressure, and possible OOM.
GC‑Based Leak Detection ( goroutineleak )
Runtime Mechanism
The runtime tracks blocked goroutines with the sudog struct, which stores pointers to the goroutine, the primitive, and wait‑queue links.
Detection Cycle
Special GC pass : an experimental GC cycle includes leak‑detection logic.
Unreachability check : identifies goroutines blocked on primitives that the GC marks as unreachable.
Status marking : such goroutines are marked with the _Gleaked state.
Result exposure : the profile is served at /debug/pprof/goroutineleak and can be inspected with the standard pprof tool.
Using the Feature
1. Install the experimental toolchain
go install golang.org/dl/gotip@latest
gotip download2. Run a demo program
The program starts a pprof server, creates a goroutine that blocks forever on an unclosed channel, and waits for a termination signal.
func main() {
go func() {
log.Printf("pprof server started at http://localhost:6060")
if err := http.ListenAndServe(":6060", nil); err != nil {
log.Fatalf("failed to start pprof server: %v", err)
}
}()
createLeakedGoroutine()
fmt.Println("Demo program running...")
fmt.Println("Use this command to check for leaks:")
fmt.Println(" GOEXPERIMENT=goroutineleakprofile gotip tool pprof http://localhost:6060/debug/pprof/goroutineleak")
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
<-sigCh
log.Println("Shutting down demo program...")
}
func createLeakedGoroutine() {
ch := make(chan int)
go func() {
fmt.Println("Leaked goroutine started - waiting for channel data")
<-ch // blocks forever
fmt.Println("Leaked goroutine should never reach this line")
}()
fmt.Println("Created a leaked goroutine - channel is now unreachable")
}Run with the experiment enabled:
GOEXPERIMENT=goroutineleakprofile gotip run main.go3. Collect the leak profile
GOEXPERIMENT=goroutineleakprofile gotip tool pprof http://localhost:6060/debug/pprof/goroutineleak4. Analyze the output
(pprof) top
(pprof) list main.createLeakedGoroutine.func1The profile shows the blocked goroutine and the source line where it receives from the unreachable channel.
Implications
Unified debugging: use familiar pprof commands without additional tools.
Faster root‑cause identification: the GC automatically pinpoints leaked goroutine locations.
Proactive detection: can be integrated into CI/CD pipelines or run in production.
Deeper runtime insight: illustrates how the GC, sudog structures, and synchronization primitives interact.
Limitations
The experiment only reports leaked goroutines; it does not free them. Developers must fix the underlying cause (e.g., close channels, keep primitives reachable).
References
goroutineleak design proposal : https://go.googlesource.com/proposal/+/master/design/74609-goroutine-leak-detection-gc.md
sudog implementation : https://github.com/golang/go/blob/c58d075e9a457fce92bdf60e2d1870c8c4df7dc5/src/runtime/runtime2.go#L406
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
