| // Copyright 2025 The Go Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style | |
| // license that can be found in the LICENSE file. | |
| package main | |
| import ( | |
| "context" | |
| "fmt" | |
| "os" | |
| "runtime" | |
| "runtime/pprof" | |
| "time" | |
| ) | |
| // Common goroutine leak patterns. Extracted from: | |
| // "Unveiling and Vanquishing Goroutine Leaks in Enterprise Microservices: A Dynamic Analysis Approach" | |
| // doi:10.1109/CGO57630.2024.10444835 | |
| // | |
| // Tests in this file are not flaky iff. the test is run with GOMAXPROCS=1. | |
| // The main goroutine forcefully yields via `runtime.Gosched()` before | |
| // running the profiler. This moves them to the back of the run queue, | |
| // allowing the leaky goroutines to be scheduled beforehand and get stuck. | |
| func init() { | |
| register("NoCloseRange", NoCloseRange) | |
| register("MethodContractViolation", MethodContractViolation) | |
| register("DoubleSend", DoubleSend) | |
| register("EarlyReturn", EarlyReturn) | |
| register("NCastLeak", NCastLeak) | |
| register("Timeout", Timeout) | |
| } | |
| // Incoming list of items and the number of workers. | |
| func noCloseRange(list []any, workers int) { | |
| ch := make(chan any) | |
| // Create each worker | |
| for i := 0; i < workers; i++ { | |
| go func() { | |
| // Each worker waits for an item and processes it. | |
| for item := range ch { | |
| // Process each item | |
| _ = item | |
| } | |
| }() | |
| } | |
| // Send each item to one of the workers. | |
| for _, item := range list { | |
| // Sending can leak if workers == 0 or if one of the workers panics | |
| ch <- item | |
| } | |
| // The channel is never closed, so workers leak once there are no more | |
| // items left to process. | |
| } | |
| func NoCloseRange() { | |
| prof := pprof.Lookup("goroutineleak") | |
| defer func() { | |
| time.Sleep(100 * time.Millisecond) | |
| prof.WriteTo(os.Stdout, 2) | |
| }() | |
| go noCloseRange([]any{1, 2, 3}, 0) | |
| go noCloseRange([]any{1, 2, 3}, 3) | |
| } | |
| // A worker processes items pushed to `ch` one by one in the background. | |
| // When the worker is no longer needed, it must be closed with `Stop`. | |
| // | |
| // Specifications: | |
| // | |
| // A worker may be started any number of times, but must be stopped only once. | |
| // Stopping a worker multiple times will lead to a close panic. | |
| // Any worker that is started must eventually be stopped. | |
| // Failing to stop a worker results in a goroutine leak | |
| type worker struct { | |
| ch chan any | |
| done chan any | |
| } | |
| // Start spawns a background goroutine that extracts items pushed to the queue. | |
| func (w worker) Start() { | |
| go func() { | |
| for { | |
| select { | |
| case <-w.ch: // Normal workflow | |
| case <-w.done: | |
| return // Shut down | |
| } | |
| } | |
| }() | |
| } | |
| func (w worker) Stop() { | |
| // Allows goroutine created by Start to terminate | |
| close(w.done) | |
| } | |
| func (w worker) AddToQueue(item any) { | |
| w.ch <- item | |
| } | |
| // worker limited in scope by workerLifecycle | |
| func workerLifecycle(items []any) { | |
| // Create a new worker | |
| w := worker{ | |
| ch: make(chan any), | |
| done: make(chan any), | |
| } | |
| // Start worker | |
| w.Start() | |
| // Operate on worker | |
| for _, item := range items { | |
| w.AddToQueue(item) | |
| } | |
| runtime.Gosched() | |
| // Exits without calling ’Stop’. Goroutine created by `Start` eventually leaks. | |
| } | |
| func MethodContractViolation() { | |
| prof := pprof.Lookup("goroutineleak") | |
| defer func() { | |
| runtime.Gosched() | |
| prof.WriteTo(os.Stdout, 2) | |
| }() | |
| workerLifecycle(make([]any, 10)) | |
| runtime.Gosched() | |
| } | |
| // doubleSend incoming channel must send a message (incoming error simulates an error generated internally). | |
| func doubleSend(ch chan any, err error) { | |
| if err != nil { | |
| // In case of an error, send nil. | |
| ch <- nil | |
| // Return is missing here. | |
| } | |
| // Otherwise, continue with normal behaviour | |
| // This send is still executed in the error case, which may lead to a goroutine leak. | |
| ch <- struct{}{} | |
| } | |
| func DoubleSend() { | |
| prof := pprof.Lookup("goroutineleak") | |
| ch := make(chan any) | |
| defer func() { | |
| runtime.Gosched() | |
| prof.WriteTo(os.Stdout, 2) | |
| }() | |
| go func() { | |
| doubleSend(ch, nil) | |
| }() | |
| <-ch | |
| go func() { | |
| doubleSend(ch, fmt.Errorf("error")) | |
| }() | |
| <-ch | |
| ch1 := make(chan any, 1) | |
| go func() { | |
| doubleSend(ch1, fmt.Errorf("error")) | |
| }() | |
| <-ch1 | |
| } | |
| // earlyReturn demonstrates a common pattern of goroutine leaks. | |
| // A return statement interrupts the evaluation of the parent goroutine before it can consume a message. | |
| // Incoming error simulates an error produced internally. | |
| func earlyReturn(err error) { | |
| // Create a synchronous channel | |
| ch := make(chan any) | |
| go func() { | |
| // Send something to the channel. | |
| // Leaks if the parent goroutine terminates early. | |
| ch <- struct{}{} | |
| }() | |
| if err != nil { | |
| // Interrupt evaluation of parent early in case of error. | |
| // Sender leaks. | |
| return | |
| } | |
| // Only receive if there is no error. | |
| <-ch | |
| } | |
| func EarlyReturn() { | |
| prof := pprof.Lookup("goroutineleak") | |
| defer func() { | |
| runtime.Gosched() | |
| prof.WriteTo(os.Stdout, 2) | |
| }() | |
| go earlyReturn(fmt.Errorf("error")) | |
| } | |
| // nCastLeak processes a number of items. First result to pass the post is retrieved from the channel queue. | |
| func nCastLeak(items []any) { | |
| // Channel is synchronous. | |
| ch := make(chan any) | |
| // Iterate over every item | |
| for range items { | |
| go func() { | |
| // Process item and send result to channel | |
| ch <- struct{}{} | |
| // Channel is synchronous: only one sender will synchronise | |
| }() | |
| } | |
| // Retrieve first result. All other senders block. | |
| // Receiver blocks if there are no senders. | |
| <-ch | |
| } | |
| func NCastLeak() { | |
| prof := pprof.Lookup("goroutineleak") | |
| defer func() { | |
| for i := 0; i < yieldCount; i++ { | |
| // Yield enough times to allow all the leaky goroutines to | |
| // reach the execution point. | |
| runtime.Gosched() | |
| } | |
| prof.WriteTo(os.Stdout, 2) | |
| }() | |
| go func() { | |
| nCastLeak(nil) | |
| }() | |
| go func() { | |
| nCastLeak(make([]any, 5)) | |
| }() | |
| } | |
| // A context is provided to short-circuit evaluation, leading | |
| // the sender goroutine to leak. | |
| func timeout(ctx context.Context) { | |
| ch := make(chan any) | |
| go func() { | |
| ch <- struct{}{} | |
| }() | |
| select { | |
| case <-ch: // Receive message | |
| // Sender is released | |
| case <-ctx.Done(): // Context was cancelled or timed out | |
| // Sender is leaked | |
| } | |
| } | |
| func Timeout() { | |
| prof := pprof.Lookup("goroutineleak") | |
| defer func() { | |
| runtime.Gosched() | |
| prof.WriteTo(os.Stdout, 2) | |
| }() | |
| ctx, cancel := context.WithCancel(context.Background()) | |
| cancel() | |
| for i := 0; i < 100; i++ { | |
| go timeout(ctx) | |
| } | |
| } | |