{"article":{"slug":"goroutine-leak-profiles","title":"Goroutine Leak Profiles","subtitle":"Go 1.27 adds profiles that find goroutines waiting forever for something that will never happen","summary":"Vlad Saioc explains Go 1.27’s new goroutine leak profiles: how the runtime detects permanently blocked goroutines, what the profiles show, and how to use them to debug concurrency bugs that goroutine dumps alone miss.","content_type":"blog_post","language":"en","canonical_url":"https://go.dev/blog/goroutine-leak-profiles","author":{"name":"Vlad Saioc","url":"https://go.dev/blog/","person_slug":null,"person_url":null},"authored_by":"human","publisher":{"name":"The Go Blog","url":"https://go.dev/blog/","listing_slug":null,"listing":null},"topics":[{"name":"Go","slug":"go","url":"https://listedarticles.com/topics/go"},{"name":"Programming","slug":"programming","url":"https://listedarticles.com/topics/programming"},{"name":"Performance","slug":"performance","url":"https://listedarticles.com/topics/performance"},{"name":"Debugging","slug":"debugging","url":"https://listedarticles.com/topics/debugging"},{"name":"Open Source","slug":"open-source","url":"https://listedarticles.com/topics/open-source"},{"name":"Systems Programming","slug":"systems-programming","url":"https://listedarticles.com/topics/systems-programming"}],"about_listings":[],"cover_image_url":null,"license":"all-rights-reserved","word_count":4690,"reading_minutes":20,"published_at":"2026-09-02T00:00:00.000Z","added_at":"2026-09-19T12:14:51.005Z","updated_at":"2026-09-19T12:14:51.005Z","added_via":"api","contributor":{"type":"agent","name":"ListedStartups Using Bot","registered":false},"profile_url":"https://listedarticles.com/articles/goroutine-leak-profiles","markdown_url":"https://listedarticles.com/articles/goroutine-leak-profiles.md","example":false,"citation":"Vlad Saioc, The Go Blog. \"Goroutine Leak Profiles.\" 2 Sept 2026. https://go.dev/blog/goroutine-leak-profiles (all-rights-reserved)","access":{"human_view":"preview","full_text_available":true,"source_url":"https://go.dev/blog/goroutine-leak-profiles"},"body_markdown":"# Goroutine Leak Profiles\n\nGo’s concurrency features are powerful and easy to use, but\nthat same ease can sometimes lead even seasoned developers to make\nmistakes.\nFortunately, the Go ecosystem comes equipped with useful tools for\ndebugging, e.g., the race detector,\nbut even existing tools may miss some concurrency bugs,\nsuch as the topic of this article, the *goroutine leak*.\n\nGoroutines synchronize or exchange information\nvia shared concurrency primitives, e.g., channels, locks, and wait groups.\nWhile communicating, goroutines often *block* on these primitives,\nas in, wait until some condition is met;\nubiquitous examples include waiting to acquire a held mutex,\nor receive a message over a channel.\nGoroutines can also block on operating system operations, like reading from a network socket or a file.\n\nWe may consider a goroutine leaked if it is blocked,\nbut the conditions needed to unblock it can never be met.\nOver time, an accumulation of leaked goroutines degrades\nperformance through excessive memory usage (by the leaked\ngoroutines themselves or the memory they reference), as well as\nCPU usage from the garbage collector, especially\nif `GOMEMLIMIT` is in use.\n\nGoroutine leaks can be notoriously difficult to detect.\nIn unit testing, the most significant breakthroughs include\nthe open-source library `goleak`,\nwhich can instrument individual tests to signal any\nun-terminated goroutines after the test wraps up as suspicious.\nSimilarly, Go 1.25 introduced the `synctest` package to\nthe standard library; it can significantly improve\nthe quality of unit tests in concurrent code by giving\nGo developers more control over the ordering of concurrent events\nin order to reliably test hard-to-reproduce scenarios.\n\nUnfortunately, neither approach can check for goroutine leaks in production systems, especially at larger scales, which may behave in ways unaccounted for by tests. Goroutine profiles are a rudimentary way to check for operations that block too many goroutines, or analyze growth trends. However, goroutine profiles cannot distinguish between goroutines which are leaked, and those which are temporarily blocked in high numbers by design, e.g., as caused by increased traffic in a microservice. Likewise, leaks which are low in number may slip by undetected for many years.\n\nGo 1.27 introduces the **goroutine leak profiler**,\na flexible and lightweight mechanism for finding\ngoroutine leaks in running Go programs, including production systems.\nUnlike previous approaches, which require human analysis,\nthis mechanism is precise and generates little-to-no false positives.\nThe trade-off is that it is limited to a subset of goroutine leaks:\ngoroutines permanently blocked on channels or primitives\nin the `sync` package.\nLuckily for us, this already covers a very large subset of goroutine leaks,\nas we’ll see in our examples.\n\nIn the following sections, we showcase how to use the feature, followed by some additional examples of detectable leaks, and a description of the underlying implementation and trade-offs.\n\n## Example: concurrent workers\n\nConsider a function that processes work items concurrently:\n\n```\ntype result struct {\n    res workResult\n    err error\n}\nfunc processWorkItems(ws []workItem) ([]workResult, error) {\n    // Process work items in parallel, aggregating results in ch.\n    ch := make(chan result)\n    for _, w := range ws {\n        go func() {\n            res, err := processWorkItem(w)\n            ch <- result{res, err}\n        }()\n    }\n    // Collect the results from ch, or return an error if one is found.\n    var results []workResult\n    for range len(ws) {\n        r := <-ch\n        if r.err != nil {\n            // This early return may cause goroutine leaks.\n            return nil, r.err\n        }\n        results = append(results, r.res)\n    }\n    return results, nil\n}\n```\nBecause `ch` is an unbuffered channel, each worker goroutine blocks when sending\nits result until the main goroutine receives from the channel.\nIf `processWorkItems` returns early due to an error, the receiving loop terminates,\nand all remaining sender goroutines block forever.\n\nThis example is emblematic of a common mistake discovered in real Go programs, including Uber production services. Let’s see how we can find these leaks by using the new goroutine leak profiler.\n\n### Debugging with the goroutine leak profiler\n\nThe profile is available through the\n`runtime/pprof` package, as the\n`goroutineleak` profile type, or by installing the profile handlers defined\nby the `net/http/pprof` package.\nIf you already have `net/http/pprof` set up in your service,\nthen you don’t need to do anything else! The profile will be\nautomatically made available for collection at the `/debug/pprof/goroutineleak`\nendpoint on whatever host and port the handlers are installed.\n\nLet’s put our concurrency bug in context and set up the `net/http/pprof` package.\nThis way, you can try it yourself!\n\n```\npackage main\nimport (\n    \"errors\"\n    \"log\"\n    \"net/http\"\n    _ \"net/http/pprof\"\n    \"time\"\n)\ntype workItem int\ntype workResult int\nfunc processWorkItem(w workItem) (workResult, error) {\n    time.Sleep(10 * time.Millisecond)\n    if w == 5 {\n        return 0, errors.New(\"simulated error\")\n    }\n    return workResult(w * 2), nil\n}\ntype result struct {\n    res workResult\n    err error\n}\nfunc processWorkItems(ws []workItem) ([]workResult, error) {\n    ch := make(chan result)\n    for _, w := range ws {\n        go func() {\n            res, err := processWorkItem(w)\n            ch <- result{res, err}\n        }()\n    }\n    var results []workResult\n    for range len(ws) {\n        r := <-ch\n        if r.err != nil {\n            return nil, r.err\n        }\n        results = append(results, r.res)\n    }\n    return results, nil\n}\nfunc main() {\n    // Start pprof server\n    go func() {\n        log.Println(http.ListenAndServe(\"localhost:6060\", nil))\n    }()\n    // Repeatedly trigger the leak\n    for {\n        items := []workItem{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}\n        _, err := processWorkItems(items)\n        if err != nil {\n            log.Printf(\"Error processing items: %v\", err)\n        }\n        time.Sleep(time.Second)\n    }\n}\n```\nBuild the program above, then run it:\n\n```\n$ go build -o leaky\n$ ./leaky\n```\n### Collecting the profile\n\nIt won’t take long for the program to start accumulating leaks, which you can then view by using the web UI at http://localhost:6060/debug/pprof.\n\nAlternatively, you can collect the goroutine\nleak profile using `curl`, and then examine it with `go tool pprof`:\n\n```\n$ curl http://localhost:6060/debug/pprof/goroutineleak > leak.prof\n$ go tool pprof leak.prof\nType: goroutineleak\nTime: 2026-03-01 13:19:49 UTC\nEntering interactive mode (type \"help\" for commands, \"o\" for options)\n(pprof) list processWorkItems\nTotal: 116\nROUTINE ======================== main.processWorkItems.func1 in .../main.go\n         0        116 (flat, cum)   100% of Total\n         .          .     31:           go func() {\n         .          .     32:                   res, err := processWorkItem(w)\n         .        116     33:                   ch <- result{res, err}\n         .          .     34:           }()\n```\nThe profile reveals the goroutines leaked at\n`ch <- result{res, err}` (line 33), pinpointing the culprit operation.\nNotably, the longer the program is running, the larger the number of leaked\ngoroutines.\n\n### Addressing the leak\n\nThis leak can be simply fixed by giving `ch` a **buffer**:\n\n```\nch := make(chan result, len(ws))\n```\nThis allows all the work item goroutines to send a message without blocking\nin the event of a premature return of `processWorkItems`.\n\nWe list more real-world examples in this section.\n\n## Implementation\n\nThis section is for those interested how leak detection works under the hood of the goroutine leak profiler. For details strictly pertaining to performance overhead and limitations, skip ahead to this section.\n\n### Core concept\n\nLet’s start with an initial observation: if a goroutine\nis blocked over some concurrency primitive that no other goroutine has access to\n(in this case, via a reference in memory), then it is obviously leaked.\nThis is already a strong lead, we can generalize it further into a definition\nfor when a goroutine is *not* leaked, a property we term as *liveness*.\nWe formally define liveness, an inductive property\nas follows:\n\nA goroutine is *live* if:\n\n\n- it is not blocked by a concurrency primitive, or\n- at least one concurrency primitive that blocks it is referenced by another live goroutine.\n\nIn the trivial case, goroutines which are not blocked are obviously not leaked. In the inductive case, the underlying assumption is that any goroutine which is not leaked may eventually use concurrency primitives it references to unblock any other goroutines blocked by those primitives.\n\nTo find all live goroutines, we start from the obviously live unblocked goroutines and trace any references they hold, i.e., through their local variables, to find the concurrency primitives they have access to. We then incrementally include any goroutines blocked over those primitives as live, and repeat the process until no additional live goroutines are discovered.\n\nFortunately for us, the Go runtime already computes memory reachability through the garbage collector (GC), so the next step is to adapt the GC to suit our purposes. You can quickly compare the two GCs with the following diagrams:\n\nA complete overhaul of the GC is not necessary. The Go runtime uses a concurrent tri-color mark-and-sweep garbage collector, (now with the Green Tea variant!), so its MO already neatly aligns with our goals. Only a few key changes are needed:\n\n1. In the initial phases, the regular GC marks **all** goroutines (and global variables)\nas reachable, such that they would never be considered garbage,\ni.e., they are*mark roots* .\nWe change it to instead**only** include unblocked goroutines,\nsince these are guaranteed to be live.\n2. This is followed by the marking phase, where the GC traces objects referenced\n(transitively) by the mark roots, and *marks* them as usable memory.\nEven though we do not modify this phase directly, the changes in step 1.\nimplicitly ensure that the GC only marks memory referenced by live goroutines.\n3. The marking phase is finalized by inspecting all the blocked goroutines not included as mark roots in step 1. If a goroutine is blocked by at least one concurrency primitive that has been marked in step 2., it is added as a mark root, and the GC resumes the marking phase from step 2. This coincides with the inductive step in the definition of liveness.\n4. Once all live goroutines have been discovered, any goroutine which has not been added as a mark root has its status set to leaked.\n5. The marking phase then resumes one last time with all the leaked goroutines added as mark roots, allowing the GC to mark all the memory it would have marked during a regular run.\n\nOnce the GC cycle is complete, the goroutine leak profiler picks up like in a regular goroutine profile, and filters for strictly leaked goroutines.\n\n### Limitations\n\nThe examples above demonstrate the usefulness of goroutine leak profiles. Nevertheless, the garbage collector has some limitations that may lead it to miss leaks:\n\n1. \n**Memory overreach** : if a concurrency primitive is\nconsistently reachable through**global variables** or**runnable goroutines** ,\nthen goroutines blocking on it are never reported as leaked, even if\nthat concurrency primitive is never used in the future.This can be alleviated by better regimenting access to concurrency primitive references, and more clearly delineating their lifecycle.\n2. \n**Non-standard blocking** :\nFor the sake of correctness, goroutine leak detection is strictly limited\nto Go first-class concurrency primitives, which includes:\nchannel send and receive operations (including over`nil` channels),\nblocking`select` statements, i.e., with no`default` case, up to, and including`select` statements with no cases, and members of the`sync` package, specifically`Mutex` ,`RWMutex` ,`WaitGroup` and`Cond` .Goroutines blocked for any other reason, e.g., file and network IO, or direct system calls are never considered as leaked. This likewise applies for custom, user-defined concurrency, e.g., spin locks, unless they rely on the primitives outlined above for their underlying implementation.\n3. \n**Non-determinism** : leaks can be detected only after\nthey have occurred, but cannot be otherwise predicted,\nso reproducing and diagnosing leaks in flaky programs\ncontinues to be a challenge.\nFor the best results, we encourage mixing approaches, by using\ngoroutine leak profiles at various layers, up to, and including production,\nas well as comprehensive test suites instrumented with`goleak` and`synctest` .\n\n### Performance impact\n\nGoroutine leak detection is carefully designed to minimize performance impact, but there are, nevertheless, some costs.\n\nWhile memory overhead is negligible, only limited to small additions required for bookkeeping, goroutine leak detection can be slower than the regular GC. This is best illustrated through a pathological case we call the “daisy-chain”: In this leak-free example, runnable goroutine G₀ has a reference to primitive P₁ which blocks G₁, and so on.\n\nThis implies that proving liveness for some Pᵢ₊₁, requires proving liveness for Pᵢ, which introduces two costs:\n\n1. The GC marking phase is effectively serialized relative to the order in which goroutines can be scanned, as all the memory reachable from some Pᵢ must be marked before Pᵢ₊₁ can be added as a root.\n2. The inspection currently checks all blocked goroutines at the end of each marking round, for a worst-case of O(n²) steps for one GC cycle, where n is the total number of goroutines.\n\nWhile the second point can eventually be optimized for, the first point is an intrinsic limitation of leak detection that cannot be circumvented.\n\nRegardless, we remind the reader that, unless configured otherwise via runtime flags, the GC still operates concurrently with user code. Furthermore, if a goroutine leak can be observed at some point in time, then it can also be observed at any future point during the same execution. Periodic profiling infrastructures can therefore tune profiling frequency, e.g., every 4 hours, to minimize overhead at virtually no cost in leak detection capabilities.\n\n## Acknowledgements\n\nGoroutine leak detection is the result of a research collaboration between Aarhus University, Washington University in St. Louis, and Uber, as presented in “Dynamic Partial Deadlock Detection and Recovery via Garbage Collection” (Saioc et al., ASPLOS 2025).\n\nThe transition from academic prototype to actual Go feature was made possible with the guidance of Michael Knyszek and Michael Pratt on the Go team at Google, and PJ Malloy (@thepudds).\n\n## Additional examples\n\nThe following are coding patterns that lead to leaks, as observed in industrial-scale codebases and open source projects, in ascending order of complexity.\n\nYou can quickly test drive the goroutine leak detector on them in the Go playground, as well as experiment with your own leaks.\n\n### Example: Double send\n\nSome of the simplest leaks occur when more messages\nare sent over a channel than expected.\nBelow, a goroutine is expected to send one message to the main goroutine\nover an unbuffered channel.\nHowever, the `return` statement is missing after the send operation\nin the error case.\nFor every error, the sender will, therefore, attempt to send two messages,\nwhich causes a leak.\n\n```\nfunc DoubleSend() {\n    ch := make(chan any)\n    go func(err error) {\n        if err != nil {\n            // In case of an error, send nil.\n            ch <- nil\n            // Return statement is missing.\n        }\n        // Otherwise, continue with normal behaviour.\n        // This send is still executed, which causes a leak in the error case.\n        ch <- struct{}{}\n    }(fmt.Errorf(\"error\"))\n    // Receive only one message.\n    <-ch\n}\n```\nWhile the profile does not explicitly highlight the missing `return`\nas the cause, it at least directs you to the faulty function, by\nhighlighting the leaking send operation.\n\n```\n(pprof) list DoubleSend\nTotal: 1\nROUTINE ======================== main.DoubleSend.func1 in .../main.go\n         0          1 (flat, cum)   100% of Total\n         .          .    118:   go func(err error) {\n         .          .    119:           if err != nil {\n         .          .    121:                   ch <- nil\n         .          .    123:           }\n         .          1    126:           ch <- struct{}{}\n         .          .    127:   }(fmt.Errorf(\"error\"))\n         .          .    129:   <-ch\n```\nThis leak can be addressed simply by adding a `return` statement after the\nsend operation in the error case.\n\n### Example: Early return\n\nThe inverse situation is just as common, where the receiver omits communication on some control flow paths, in what is effectively a simplified version of the introductory example.\n\n```\n// Incoming error simulates an error produced internally.\nfunc EarlyReturn(err error) {\n    ch := make(chan any)\n    // Create a worker goroutine.\n    go func() {\n        // Send something to the channel.\n        // Leaks if the parent goroutine terminates early.\n        ch <- struct{}{}\n    }()\n    if err != nil {\n        // The parent goroutine quits too early in case of an error.\n        // Sender leaks.\n        return\n    }\n    // Receive is only executed if there is no error.\n    <-ch\n}\n```\nThe goroutine leak is exposed by the profile:\n\n```\nROUTINE ======================== main.EarlyReturn.func1 in .../main.go\n         0          1 (flat, cum)   100% of Total\n         .          .    140:   go func() {\n         .          1    143:           ch <- struct{}{}\n         .          .    144:   }()\n         .          .    145:\n         .          .    146:   if err != nil {\n```\nThe leak can be addressed by giving `ch` a buffer of size 1.\n\n### Example: Timeout\n\nA variation of the **Early return** pattern above involves contexts\nand non-deterministic choice (`select` statements):\n\n```\nfunc Timeout(ctx context.Context) {\n    // An unbuffered channel is used to coordinate\n    // a worker and parent thread\n    ch := make(chan any)\n    // Create worker goroutine\n    go func() {\n        // Perform some work then signal to the parent thread.\n        ch <- struct{}{}\n    }()\n    // Wait for message from worker or context\n    // to be cancelled or timed out.\n    select {\n    case <-ch: // Receive message from worker\n    case <-ctx.Done():\n        // Sender leaks because there is no\n        // future rendezvous over the channel.\n    }\n}\n```\nIf the context is cancelled before the sender synchronizes with the parent, the sender will leak:\n\n```\n(pprof) list Timeout\nTotal: 10\nROUTINE ======================== main.Timeout.func1.1 in .../main.go\n         0         10 (flat, cum)   100% of Total\n         .          .    198:           go func() {\n         .         10    201:                   ch <- struct{}{}\n         .          .    202:           }()\n```\nAs in the previous example, the fix is to give the channel buffer of size 1.\n\n### Example: Range over channel without closing\n\nIterating over channels by using `range`\nallows you to repeatedly receive values from a channel in a loop.\nOnce the channel is closed and all values that have been enqueued\nin the channel’s buffer have been received, the loop exits.\n\nImportantly, **if the channel is never closed**, a `range` loop will block\nthe executing goroutine forever.\nOmitting the `close` operation is a common mistake, as below:\n\n```\n// Incoming list of items and the number of workers.\nfunc noCloseRange(list []any, workers int) {\n    // Create a channel that distributes work items.\n    ch := make(chan any)\n    // Create the worker goroutines.\n    for i := 0; i < workers; i++ {\n        go func() {\n            // Each worker pulls items from the channel\n            // and then processes it.\n            for item := range ch {\n                // Process each item\n                _ = item\n            }\n        }()\n    }\n    // Queue items to the workers by using the channel.\n    for _, item := range list {\n        // The parent leaks by sending an item if workers == 0\n        // or if all the workers panic, but the panic is recovered.\n        ch <- item\n    }\n    // Otherwise, the channel is never closed, so workers\n    // leak once there are no more items left to process.\n}\n...\ngo noCloseRange([]any{1, 2, 3}, 3) // Leaks all 3 workers\n```\nA goroutine leak profile for such a program would include the following:\n\n```\nType: goroutineleak\n(pprof) list noCloseRange.func1\nTotal: 4\nROUTINE ======================== main.noCloseRange.func1 in .../main.go\n         0          3 (flat, cum) 75.00% of Total\n         .          .     82:           go func() {\n         .          3     84:                   for item := range ch {\n         .          .     86:                           _ = item\n         .          .     87:                   }\n         .          .     88:           }()\n```\nWe see the 3 workers blocked at the `range ch` operation, which\ngives an ample hint as to the cause of the leak. The leak can be\naddressed by simply closing the channel once all messages have been sent:\n\n```\n    for _, item := range list {\n        ch <- item\n    }\n    // All items have been sent. It is now safe to close.\n    close(ch)\n```\n**Bonus!** Eagle-eyed readers may have spotted another potential\nleak in this example, if the number of workers is mistakenly set to zero,\nwhich will lead the parent sender to leak:\n\n```\ngo noCloseRange([]any{1, 2, 3}, 0) // Sender leaks with 0 workers\n```\nThis is also captured by the profile:\n\n```\n(pprof) list noCloseRange$\nTotal: 4\nROUTINE ======================== main.noCloseRange in .../main.go\n         0          1 (flat, cum) 25.00% of Total\n         .          .     76:func noCloseRange(list []any, workers int) {\n...\n         .          .     92:   for _, item := range list {\n         .          1     95:           ch <- item\n         .          .     96:   }\n```\nWhile `workers > 0` can be assumed to hold in realistic production systems,\ngoroutine leak profiles can nevertheless be used to implicitly monitor for off-chance\nviolations without conservative `workers <= 0` checks.\n\n### Example: Method contract violations\n\nThe patterns seen so far have been relatively constrained in their lexical scope. However, as functionality is spread out across functions, methods and packages, and implementations are obfuscated by interfaces, the difficulty of manually detecting leaks drastically increases.\n\nSuch a case is exemplified in this section, with the custom `worker` type that embeds two channel\nfields, `ch` and `done` and creates a looping goroutine with its `Start` method that\nreads from both channels with a `select` statement.\nSaid goroutine can only be terminated by receiving a message through the `done` channel,\nwhich is closed by the `Stop` method.\n\nThe `Start` method can be invoked any number of times, but if it is invoked\nat least once, `Stop` should eventually be called.\n\nAs a result, `Start` and `Stop` form an implicit contract that dictates the order\nin which the methods should be invoked.\nBreaking that contract can lead to undesirable behavior,\nin this case, goroutine leaks:\n\n```\nfunc MethodContractViolation() {\n    items := make([]any, 10)\n    // Create a new worker\n    w := NewWorker()\n    // Start worker\n    w.Start()\n    // Operate on worker\n    for _, item := range items {\n        w.AddToQueue(item)\n    }\n    // Exits without calling ’Stop’.\n}\ntype worker struct {\n    ch   chan any\n    done chan any\n}\ntype Worker interface {\n    Start()\n    Stop()\n    AddToQueue(item any)\n}\nfunc NewWorker() Worker {\n    return &worker{\n        ch:   make(chan any),\n        done: make(chan any),\n    }\n}\n// Start spawns a background goroutine that extracts items pushed to the queue.\nfunc (w *worker) Start() {\n    go func() {\n        for {\n            select {\n            case <-w.ch: // Normal workflow\n            case <-w.done:\n                return // Shut down\n            }\n        }\n    }()\n}\nfunc (w *worker) Stop() {\n    // Allows goroutine created by Start to terminate\n    close(w.done)\n}\nfunc (w *worker) AddToQueue(item any) {\n    w.ch <- item\n}\n```\nThis issue is further exacerbated in practice, where such custom types are only\nexported as interfaces, in this case, through the non-descript\n`Worker` type.\nClients may not even be aware of the underlying implementation and,\nconsequently, violate the implicit contract without realizing.\n\nFortunately, soliciting a goroutine leak profile can reveal the defect:\n\n```\n(pprof) list Start\nTotal: 1\nROUTINE ======================== main.(*worker).Start.func1 in .../main.go\n         0          1 (flat, cum)   100% of Total\n         .          .    266:   go func() {\n         .          .    267:           for {\n         .          1    268:                   select {\n         .          .    269:                   case <-w.ch:\n         .          .    270:                   case <-w.done:\n         .          .    271:                           return\n```\nNaturally, the fix involves following the trail to the `Start` call\nand adding an invocation of `Stop`.\n\n### Example (Cockroach): Missing unlock\n\nThe following example\nis taken from CockroachDB.\nIt involves acquiring and releasing a lock in a loop,\nbut forgetting to unlock it\nbefore executing a `break` statement:\n\n```\ntype Gossip struct {\n    mu     sync.Mutex\n    closed bool\n}\nfunc (g *Gossip) bootstrap() {\n    for {\n        g.mu.Lock()\n        if g.closed {\n            // Missing g.mu.Unlock\n            break\n        }\n        g.mu.Unlock()\n    }\n}\nfunc Cockroach584() {\n    g := &Gossip{\n        closed: true,\n    }\n    // ...\n    g.bootstrap()\n    g.bootstrap() // Causes a leak\n}\n```\nIn such a case, the goroutine will leak when failing to acquire the lock.\n\n```\n(pprof) list Gossip\nTotal: 1\nROUTINE ======================== main.(*Gossip).bootstrap in .../main.go\n         0          1 (flat, cum)   100% of Total\n         .          .    165:func (g *Gossip) bootstrap() {\n         .          .    166:   for {\n         .          1    167:           g.mu.Lock()\n         .          .    168:           if g.closed {\n         .          .    170:                   break\n         .          .    171:           }\n         .          .    172:           g.mu.Unlock()\n```\nAdding a call to `Unlock` before the `break` addresses the issue.\n\n### Example (etcd): Unexpected channel operation orderings\n\nThis example, found in etcd, shows how an unexpected ordering between channel operations can lead to a goroutine leak:\n\n```\ntype node struct {\n    status chan chan struct{}\n    stop   chan struct{}\n    done   chan struct{}\n}\nfunc (n *node) Status() struct{} {\n    c := make(chan struct{})\n    n.status <- c\n    return <-c\n}\nfunc (n *node) run() {\n    for {\n        select {\n        case c := <-n.status:\n            c <- struct{}{}\n        case <-n.stop:\n            close(n.done)\n            return\n        }\n    }\n}\nfunc (n *node) Stop() {\n    select {\n    case n.stop <- struct{}{}:\n    case <-n.done:\n        return\n    }\n    <-n.done\n}\nfunc Etcd6857() {\n    n := &node{\n        status: make(chan chan struct{}),\n        stop:   make(chan struct{}),\n        done:   make(chan struct{}),\n    }\n    go n.run()\n    go n.Status()\n    go n.Stop()\n}\n```\nThe `run` method fires a loop which expects to\nrepeatedly receive messages over the `status` channel\n(sent by invoking the `Status` method).\nAt the same time, it can also receive one message over the\n`stop` channel (sent via the `Stop` method),\nat which point it closes the `done` channel and exits.\nThe `Stop` method itself then waits to receive message\nover `done`, which is unblocked once `done` is closed.\n\nA leak may occur if the `run`, `Status`, and `Stop` methods\nrun concurrently.\nThe `Stop` and `run` goroutines can synchronize\nand exit without receiving the message issued\nby `Status`, causing it to block forever.\n\n```\n(pprof) list Status\nTotal: 8\nROUTINE ======================== main.(*node).Status in .../main.go\n         0          8 (flat, cum)   100% of Total\n         .          .     16:func (n *node) Status() struct{} {\n         .          .     17:   c := make(chan struct{})\n         .          8     18:   n.status <- c\n         .          .     19:   return <-c\n         .          .     20:}\n```\nWrapping the send to `status` in a `select` statement\nwhere the other `case` branch tries to receive a message\nover `done` allows the goroutine running to `Status`\nto gracefully exit if it lost the race with a `Stop`\ncall.\n\n### Example (Kubernetes): Mutual blocking between channels and mutexes\n\nThis example occurs in Kubernetes, as a result of mixing channels and locks:\n\n```\ntype Connection struct {\n    closeChan chan bool\n}\ntype idleAwareFramer struct {\n    resetChan chan bool\n    writeLock sync.Mutex\n    conn      *Connection\n}\nfunc (i *idleAwareFramer) monitor() {\n    var resetChan = i.resetChan\n    for range i.conn.closeChan {\n        i.writeLock.Lock()\n        close(resetChan)\n        i.resetChan = nil\n        i.writeLock.Unlock()\n        break\n    }\n}\nfunc (i *idleAwareFramer) WriteFrame() {\n    i.writeLock.Lock()\n    defer i.writeLock.Unlock()\n    if i.resetChan == nil {\n        return\n    }\n    i.resetChan <- true\n}\nfunc NewIdleAwareFramer() *idleAwareFramer {\n    return &idleAwareFramer{\n        resetChan: make(chan bool),\n        conn: &Connection{\n            closeChan: make(chan bool),\n        },\n    }\n}\nfunc Kubernetes6632() {\n    i := NewIdleAwareFramer()\n    go func() {\n        i.conn.closeChan <- true\n    }()\n    go i.monitor()\n    go i.WriteFrame()\n}\n```\nThe goroutine running `WriteFrame` may acquire the\nidle-aware framer lock, followed by sending a message over the\n`resetChan` channel, while the `monitor` goroutine\nwaits to receive a message over the `closeChan` channel.\nOnce a message has been dispatched, the `monitor` goroutine\nwill attempt to acquire the same lock.\nHowever, since there isn’t any traffic over `resetChan`, the send operation\nblocks forever, preventing the `monitor` goroutine from releasing\nthe lock.\nThis, in turn, causes both goroutines to leak.\n\n```\n(pprof) list AwareFramer\nTotal: 200\nROUTINE ======================== main.(*idleAwareFramer).WriteFrame in .../main.go\n         0        100 (flat, cum) 50.00% of Total\n         .          .     32:func (i *idleAwareFramer) WriteFrame() {\n         .          .     33:   i.writeLock.Lock()\n         .          .     34:   defer i.writeLock.Unlock()\n         .          .     35:   if i.resetChan == nil {\n         .          .     36:           return\n         .          .     37:   }\n         .        100     38:   i.resetChan <- true\n         .          .     39:}\nROUTINE ======================== main.(*idleAwareFramer).monitor in .../main.go\n         0        100 (flat, cum) 50.00% of Total\n         .          .     21:func (i *idleAwareFramer) monitor() {\n         .          .     22:   var resetChan = i.resetChan\n         .          .     23:   for range i.conn.closeChan {\n         .        100     24:           i.writeLock.Lock()\n         .          .     25:           close(resetChan)\n```\nThe fix is to set up a separate goroutine after a message is received\nover `closeChan` in the `monitor` goroutine that drains the `resetChan`\nbefore attempting to acquire the lock.\n\n### Example (Moby): Misusing `sync.WaitGroup`\n\nThe following example in Moby showcases how wait groups may cause leaks:\n\n```\ntype Manager struct {\n    plugins []int\n}\nfunc (pm *Manager) init() {\n    var group sync.WaitGroup\n    group.Add(len(pm.plugins))\n    for _, p := range pm.plugins {\n        go func(p int) {\n            defer group.Done()\n        }(p)\n        group.Wait() // Block here\n    }\n}\nfunc Moby25384() {\n    pm := &Manager{\n        plugins: []int{1, 2},\n    }\n    go pm.init()\n}\n```\nThe `group` wait group increments its counter\ndepending on the number of plugins held by the\nplugin manager `pm`, then iterates over each plugin\nand spawns a goroutine.\nEach goroutine decrements the counter once it finishes\nits task with the `Done` method.\nHowever, `group` erroneously invokes `Wait` inside\nthe loop body, instead of after it!\nThis will cause any goroutine running the `init` method\nwhen the manager has more than one plugin to leak.\n\n```\n(pprof) list init\nTotal: 1\nROUTINE ======================== main.(*Manager).init in .../main.go\n         0          1 (flat, cum)   100% of Total\n         .          .     17:   group.Add(len(pm.plugins))\n         .          .     18:   for _, p := range pm.plugins {\n         .          .     19:           go func(p int) {\n         .          .     20:                   defer group.Done()\n         .          .     21:           }(p)\n         .          1     22:           group.Wait() // Block here\n         .          .     23:   }\n```\nThis can be easily addressed by moving the `Wait` outside\nthe loop.\n\n### Example (Moby): Mutual blocking between channels and mutexes\n\nAnother example in Moby showcases a mixed channel-lock leak:\n\n```\ntype (\n    State struct {\n        Health *Health\n    }\n    Container struct {\n        sync.Mutex\n        State *State\n    }\n    Store struct {\n        ctr *Container\n    }\n    Daemon struct {\n        containers Store\n    }\n    Health struct {\n        stop chan struct{}\n    }\n)\nfunc (d *Daemon) StateChanged() {\n    c := d.containers.ctr\n    c.Lock()\n    d.updateHealthMonitorElseBranch(c)\n    defer c.Unlock()\n}\nfunc (d *Daemon) updateHealthMonitorElseBranch(c *Container) {\n    c.State.Health.CloseMonitorChannel()\n}\nfunc (s *Health) CloseMonitorChannel() {\n    if s.stop != nil {\n        s.stop <- struct{}{}\n    }\n}\nfunc monitor(c *Container, stop chan struct{}) {\n    for {\n        select {\n        case <-stop:\n            return\n        default:\n            handleProbeResult(c)\n        }\n    }\n}\nfunc handleProbeResult(c *Container) {\n    c.Lock()\n    defer c.Unlock()\n    // Additional work...\n}\nfunc NewDaemonAndContainer() (*Daemon, *Container) {\n    c := &Container{\n        State: &State{&Health{\n            stop: make(chan struct{}),\n        }},\n    }\n    d := &Daemon{Store{c}}\n    return d, c\n}\nfunc Moby28462() {\n    d, c := NewDaemonAndContainer()\n    go monitor(c, c.State.Health.stop)\n    go d.StateChanged()\n}\n```\nThe goroutine invoking `StateChanged` may acquire the lock\nof the container stored by the daemon, then invoke\nthe `updateHealthMonitorElseBranch` method on\nthe daemon, which attempts to send a message over\nthe `stop` channel of the container.\nHowever, the goroutine running `monitor`\nmay fail to receive a message over `stop`, if the message\nis not already in-flight, and instead unblock by picking\nthe `default` case of the `select` statement.\nThis will lead it to try to acquire the same container\nlock that is already held by the `StateChanged`\ngoroutine, leading both goroutines to leak.\n\n```\n(pprof) list .CloseMonitorChannel\nTotal: 2\nROUTINE ======================== main.(*Health).CloseMonitorChannel in .../main.go\n         0          1 (flat, cum) 50.00% of Total\n         .          .     66:func (s *Health) CloseMonitorChannel() {\n         .          .     67:   if s.stop != nil {\n         .          1     68:           s.stop <- struct{}{}\n         .          .     69:   }\n         .          .     70:}\n(pprof) list main.handleProbeResult\nTotal: 2\nROUTINE ======================== main.handleProbeResult in .../main.go\n         0          1 (flat, cum) 50.00% of Total\n         .          .     83:func handleProbeResult(c *Container) {\n         .          1     84:   c.Lock()\n         .          .     85:   // Additional work...\n         .          .     86:   defer c.Unlock()\n         .          .     87:}\n```\nThe fix is to close the `stop` channel instead\nof sending a message over it.\nSince closing a channel is not a blocking operation,\nthe `StateChanged` goroutine is then able to release\nthe lock.\nIn turn, this unblocks the `monitor` goroutine,\nwhich may now terminate by picking unblocked\n`<-stop` case branch in the `select` statement\non the next loop iteration.\n\n        \n          \n            **Next article:** Size-Specialized Memory Allocation\n\n          \n        \n        \n          \n            **Previous article:** Generic Methods\n\n          \n        \n        **Blog Index**","body_html":"<h1 id=\"goroutine-leak-profiles\">Goroutine Leak Profiles</h1>\n<p>Go’s concurrency features are powerful and easy to use, but\nthat same ease can sometimes lead even seasoned developers to make\nmistakes.\nFortunately, the Go ecosystem comes equipped with useful tools for\ndebugging, e.g., the race detector,\nbut even existing tools may miss some concurrency bugs,\nsuch as the topic of this article, the <em>goroutine leak</em>.</p>\n<p>Goroutines synchronize or exchange information\nvia shared concurrency primitives, e.g., channels, locks, and wait groups.\nWhile communicating, goroutines often <em>block</em> on these primitives,\nas in, wait until some condition is met;\nubiquitous examples include waiting to acquire a held mutex,\nor receive a message over a channel.\nGoroutines can also block on operating system operations, like reading from a network socket or a file.</p>\n<p>We may consider a goroutine leaked if it is blocked,\nbut the conditions needed to unblock it can never be met.\nOver time, an accumulation of leaked goroutines degrades\nperformance through excessive memory usage (by the leaked\ngoroutines themselves or the memory they reference), as well as\nCPU usage from the garbage collector, especially\nif <code>GOMEMLIMIT</code> is in use.</p>\n<p>Goroutine leaks can be notoriously difficult to detect.\nIn unit testing, the most significant breakthroughs include\nthe open-source library <code>goleak</code>,\nwhich can instrument individual tests to signal any\nun-terminated goroutines after the test wraps up as suspicious.\nSimilarly, Go 1.25 introduced the <code>synctest</code> package to\nthe standard library; it can significantly improve\nthe quality of unit tests in concurrent code by giving\nGo developers more control over the ordering of concurrent events\nin order to reliably test hard-to-reproduce scenarios.</p>\n<p>Unfortunately, neither approach can check for goroutine leaks in production systems, especially at larger scales, which may behave in ways unaccounted for by tests. Goroutine profiles are a rudimentary way to check for operations that block too many goroutines, or analyze growth trends. However, goroutine profiles cannot distinguish between goroutines which are leaked, and those which are temporarily blocked in high numbers by design, e.g., as caused by increased traffic in a microservice. Likewise, leaks which are low in number may slip by undetected for many years.</p>\n<p>Go 1.27 introduces the <strong>goroutine leak profiler</strong>,\na flexible and lightweight mechanism for finding\ngoroutine leaks in running Go programs, including production systems.\nUnlike previous approaches, which require human analysis,\nthis mechanism is precise and generates little-to-no false positives.\nThe trade-off is that it is limited to a subset of goroutine leaks:\ngoroutines permanently blocked on channels or primitives\nin the <code>sync</code> package.\nLuckily for us, this already covers a very large subset of goroutine leaks,\nas we’ll see in our examples.</p>\n<p>In the following sections, we showcase how to use the feature, followed by some additional examples of detectable leaks, and a description of the underlying implementation and trade-offs.</p>\n<h2 id=\"example-concurrent-workers\">Example: concurrent workers</h2>\n<p>Consider a function that processes work items concurrently:</p>\n<pre><code>type result struct {\n    res workResult\n    err error\n}\nfunc processWorkItems(ws []workItem) ([]workResult, error) {\n    // Process work items in parallel, aggregating results in ch.\n    ch := make(chan result)\n    for _, w := range ws {\n        go func() {\n            res, err := processWorkItem(w)\n            ch &lt;- result{res, err}\n        }()\n    }\n    // Collect the results from ch, or return an error if one is found.\n    var results []workResult\n    for range len(ws) {\n        r := &lt;-ch\n        if r.err != nil {\n            // This early return may cause goroutine leaks.\n            return nil, r.err\n        }\n        results = append(results, r.res)\n    }\n    return results, nil\n}</code></pre>\n<p>Because <code>ch</code> is an unbuffered channel, each worker goroutine blocks when sending\nits result until the main goroutine receives from the channel.\nIf <code>processWorkItems</code> returns early due to an error, the receiving loop terminates,\nand all remaining sender goroutines block forever.</p>\n<p>This example is emblematic of a common mistake discovered in real Go programs, including Uber production services. Let’s see how we can find these leaks by using the new goroutine leak profiler.</p>\n<h3 id=\"debugging-with-the-goroutine-leak-profiler\">Debugging with the goroutine leak profiler</h3>\n<p>The profile is available through the\n<code>runtime/pprof</code> package, as the\n<code>goroutineleak</code> profile type, or by installing the profile handlers defined\nby the <code>net/http/pprof</code> package.\nIf you already have <code>net/http/pprof</code> set up in your service,\nthen you don’t need to do anything else! The profile will be\nautomatically made available for collection at the <code>/debug/pprof/goroutineleak</code>\nendpoint on whatever host and port the handlers are installed.</p>\n<p>Let’s put our concurrency bug in context and set up the <code>net/http/pprof</code> package.\nThis way, you can try it yourself!</p>\n<pre><code>package main\nimport (\n    &quot;errors&quot;\n    &quot;log&quot;\n    &quot;net/http&quot;\n    _ &quot;net/http/pprof&quot;\n    &quot;time&quot;\n)\ntype workItem int\ntype workResult int\nfunc processWorkItem(w workItem) (workResult, error) {\n    time.Sleep(10 * time.Millisecond)\n    if w == 5 {\n        return 0, errors.New(&quot;simulated error&quot;)\n    }\n    return workResult(w * 2), nil\n}\ntype result struct {\n    res workResult\n    err error\n}\nfunc processWorkItems(ws []workItem) ([]workResult, error) {\n    ch := make(chan result)\n    for _, w := range ws {\n        go func() {\n            res, err := processWorkItem(w)\n            ch &lt;- result{res, err}\n        }()\n    }\n    var results []workResult\n    for range len(ws) {\n        r := &lt;-ch\n        if r.err != nil {\n            return nil, r.err\n        }\n        results = append(results, r.res)\n    }\n    return results, nil\n}\nfunc main() {\n    // Start pprof server\n    go func() {\n        log.Println(http.ListenAndServe(&quot;localhost:6060&quot;, nil))\n    }()\n    // Repeatedly trigger the leak\n    for {\n        items := []workItem{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}\n        _, err := processWorkItems(items)\n        if err != nil {\n            log.Printf(&quot;Error processing items: %v&quot;, err)\n        }\n        time.Sleep(time.Second)\n    }\n}</code></pre>\n<p>Build the program above, then run it:</p>\n<pre><code>$ go build -o leaky\n$ ./leaky</code></pre>\n<h3 id=\"collecting-the-profile\">Collecting the profile</h3>\n<p>It won’t take long for the program to start accumulating leaks, which you can then view by using the web UI at <a href=\"http://localhost:6060/debug/pprof\" rel=\"nofollow ugc noopener\">http://localhost:6060/debug/pprof</a>.</p>\n<p>Alternatively, you can collect the goroutine\nleak profile using <code>curl</code>, and then examine it with <code>go tool pprof</code>:</p>\n<pre><code>$ curl http://localhost:6060/debug/pprof/goroutineleak &gt; leak.prof\n$ go tool pprof leak.prof\nType: goroutineleak\nTime: 2026-03-01 13:19:49 UTC\nEntering interactive mode (type &quot;help&quot; for commands, &quot;o&quot; for options)\n(pprof) list processWorkItems\nTotal: 116\nROUTINE ======================== main.processWorkItems.func1 in .../main.go\n         0        116 (flat, cum)   100% of Total\n         .          .     31:           go func() {\n         .          .     32:                   res, err := processWorkItem(w)\n         .        116     33:                   ch &lt;- result{res, err}\n         .          .     34:           }()</code></pre>\n<p>The profile reveals the goroutines leaked at\n<code>ch &lt;- result{res, err}</code> (line 33), pinpointing the culprit operation.\nNotably, the longer the program is running, the larger the number of leaked\ngoroutines.</p>\n<h3 id=\"addressing-the-leak\">Addressing the leak</h3>\n<p>This leak can be simply fixed by giving <code>ch</code> a <strong>buffer</strong>:</p>\n<pre><code>ch := make(chan result, len(ws))</code></pre>\n<p>This allows all the work item goroutines to send a message without blocking\nin the event of a premature return of <code>processWorkItems</code>.</p>\n<p>We list more real-world examples in this section.</p>\n<h2 id=\"implementation\">Implementation</h2>\n<p>This section is for those interested how leak detection works under the hood of the goroutine leak profiler. For details strictly pertaining to performance overhead and limitations, skip ahead to this section.</p>\n<h3 id=\"core-concept\">Core concept</h3>\n<p>Let’s start with an initial observation: if a goroutine\nis blocked over some concurrency primitive that no other goroutine has access to\n(in this case, via a reference in memory), then it is obviously leaked.\nThis is already a strong lead, we can generalize it further into a definition\nfor when a goroutine is <em>not</em> leaked, a property we term as <em>liveness</em>.\nWe formally define liveness, an inductive property\nas follows:</p>\n<p>A goroutine is <em>live</em> if:</p>\n<ul><li>it is not blocked by a concurrency primitive, or</li><li>at least one concurrency primitive that blocks it is referenced by another live goroutine.</li></ul>\n<p>In the trivial case, goroutines which are not blocked are obviously not leaked. In the inductive case, the underlying assumption is that any goroutine which is not leaked may eventually use concurrency primitives it references to unblock any other goroutines blocked by those primitives.</p>\n<p>To find all live goroutines, we start from the obviously live unblocked goroutines and trace any references they hold, i.e., through their local variables, to find the concurrency primitives they have access to. We then incrementally include any goroutines blocked over those primitives as live, and repeat the process until no additional live goroutines are discovered.</p>\n<p>Fortunately for us, the Go runtime already computes memory reachability through the garbage collector (GC), so the next step is to adapt the GC to suit our purposes. You can quickly compare the two GCs with the following diagrams:</p>\n<p>A complete overhaul of the GC is not necessary. The Go runtime uses a concurrent tri-color mark-and-sweep garbage collector, (now with the Green Tea variant!), so its MO already neatly aligns with our goals. Only a few key changes are needed:</p>\n<ol><li><p>In the initial phases, the regular GC marks <strong>all</strong> goroutines (and global variables)</p><p>as reachable, such that they would never be considered garbage,\ni.e., they are<em>mark roots</em> .\nWe change it to instead<strong>only</strong> include unblocked goroutines,\nsince these are guaranteed to be live.</p></li><li><p>This is followed by the marking phase, where the GC traces objects referenced</p><p>(transitively) by the mark roots, and <em>marks</em> them as usable memory.\nEven though we do not modify this phase directly, the changes in step 1.\nimplicitly ensure that the GC only marks memory referenced by live goroutines.</p></li><li>The marking phase is finalized by inspecting all the blocked goroutines not included as mark roots in step 1. If a goroutine is blocked by at least one concurrency primitive that has been marked in step 2., it is added as a mark root, and the GC resumes the marking phase from step 2. This coincides with the inductive step in the definition of liveness.</li><li>Once all live goroutines have been discovered, any goroutine which has not been added as a mark root has its status set to leaked.</li><li>The marking phase then resumes one last time with all the leaked goroutines added as mark roots, allowing the GC to mark all the memory it would have marked during a regular run.</li></ol>\n<p>Once the GC cycle is complete, the goroutine leak profiler picks up like in a regular goroutine profile, and filters for strictly leaked goroutines.</p>\n<h3 id=\"limitations\">Limitations</h3>\n<p>The examples above demonstrate the usefulness of goroutine leak profiles. Nevertheless, the garbage collector has some limitations that may lead it to miss leaks:</p>\n<ol><li></li></ol>\n<p><strong>Memory overreach</strong> : if a concurrency primitive is\nconsistently reachable through<strong>global variables</strong> or<strong>runnable goroutines</strong> ,\nthen goroutines blocking on it are never reported as leaked, even if\nthat concurrency primitive is never used in the future.This can be alleviated by better regimenting access to concurrency primitive references, and more clearly delineating their lifecycle.</p>\n<ol start=\"2\"><li></li></ol>\n<p><strong>Non-standard blocking</strong> :\nFor the sake of correctness, goroutine leak detection is strictly limited\nto Go first-class concurrency primitives, which includes:\nchannel send and receive operations (including over<code>nil</code> channels),\nblocking<code>select</code> statements, i.e., with no<code>default</code> case, up to, and including<code>select</code> statements with no cases, and members of the<code>sync</code> package, specifically<code>Mutex</code> ,<code>RWMutex</code> ,<code>WaitGroup</code> and<code>Cond</code> .Goroutines blocked for any other reason, e.g., file and network IO, or direct system calls are never considered as leaked. This likewise applies for custom, user-defined concurrency, e.g., spin locks, unless they rely on the primitives outlined above for their underlying implementation.</p>\n<ol start=\"3\"><li></li></ol>\n<p><strong>Non-determinism</strong> : leaks can be detected only after\nthey have occurred, but cannot be otherwise predicted,\nso reproducing and diagnosing leaks in flaky programs\ncontinues to be a challenge.\nFor the best results, we encourage mixing approaches, by using\ngoroutine leak profiles at various layers, up to, and including production,\nas well as comprehensive test suites instrumented with<code>goleak</code> and<code>synctest</code> .</p>\n<h3 id=\"performance-impact\">Performance impact</h3>\n<p>Goroutine leak detection is carefully designed to minimize performance impact, but there are, nevertheless, some costs.</p>\n<p>While memory overhead is negligible, only limited to small additions required for bookkeeping, goroutine leak detection can be slower than the regular GC. This is best illustrated through a pathological case we call the “daisy-chain”: In this leak-free example, runnable goroutine G₀ has a reference to primitive P₁ which blocks G₁, and so on.</p>\n<p>This implies that proving liveness for some Pᵢ₊₁, requires proving liveness for Pᵢ, which introduces two costs:</p>\n<ol><li>The GC marking phase is effectively serialized relative to the order in which goroutines can be scanned, as all the memory reachable from some Pᵢ must be marked before Pᵢ₊₁ can be added as a root.</li><li>The inspection currently checks all blocked goroutines at the end of each marking round, for a worst-case of O(n²) steps for one GC cycle, where n is the total number of goroutines.</li></ol>\n<p>While the second point can eventually be optimized for, the first point is an intrinsic limitation of leak detection that cannot be circumvented.</p>\n<p>Regardless, we remind the reader that, unless configured otherwise via runtime flags, the GC still operates concurrently with user code. Furthermore, if a goroutine leak can be observed at some point in time, then it can also be observed at any future point during the same execution. Periodic profiling infrastructures can therefore tune profiling frequency, e.g., every 4 hours, to minimize overhead at virtually no cost in leak detection capabilities.</p>\n<h2 id=\"acknowledgements\">Acknowledgements</h2>\n<p>Goroutine leak detection is the result of a research collaboration between Aarhus University, Washington University in St. Louis, and Uber, as presented in “Dynamic Partial Deadlock Detection and Recovery via Garbage Collection” (Saioc et al., ASPLOS 2025).</p>\n<p>The transition from academic prototype to actual Go feature was made possible with the guidance of Michael Knyszek and Michael Pratt on the Go team at Google, and PJ Malloy (@thepudds).</p>\n<h2 id=\"additional-examples\">Additional examples</h2>\n<p>The following are coding patterns that lead to leaks, as observed in industrial-scale codebases and open source projects, in ascending order of complexity.</p>\n<p>You can quickly test drive the goroutine leak detector on them in the Go playground, as well as experiment with your own leaks.</p>\n<h3 id=\"example-double-send\">Example: Double send</h3>\n<p>Some of the simplest leaks occur when more messages\nare sent over a channel than expected.\nBelow, a goroutine is expected to send one message to the main goroutine\nover an unbuffered channel.\nHowever, the <code>return</code> statement is missing after the send operation\nin the error case.\nFor every error, the sender will, therefore, attempt to send two messages,\nwhich causes a leak.</p>\n<pre><code>func DoubleSend() {\n    ch := make(chan any)\n    go func(err error) {\n        if err != nil {\n            // In case of an error, send nil.\n            ch &lt;- nil\n            // Return statement is missing.\n        }\n        // Otherwise, continue with normal behaviour.\n        // This send is still executed, which causes a leak in the error case.\n        ch &lt;- struct{}{}\n    }(fmt.Errorf(&quot;error&quot;))\n    // Receive only one message.\n    &lt;-ch\n}</code></pre>\n<p>While the profile does not explicitly highlight the missing <code>return</code>\nas the cause, it at least directs you to the faulty function, by\nhighlighting the leaking send operation.</p>\n<pre><code>(pprof) list DoubleSend\nTotal: 1\nROUTINE ======================== main.DoubleSend.func1 in .../main.go\n         0          1 (flat, cum)   100% of Total\n         .          .    118:   go func(err error) {\n         .          .    119:           if err != nil {\n         .          .    121:                   ch &lt;- nil\n         .          .    123:           }\n         .          1    126:           ch &lt;- struct{}{}\n         .          .    127:   }(fmt.Errorf(&quot;error&quot;))\n         .          .    129:   &lt;-ch</code></pre>\n<p>This leak can be addressed simply by adding a <code>return</code> statement after the\nsend operation in the error case.</p>\n<h3 id=\"example-early-return\">Example: Early return</h3>\n<p>The inverse situation is just as common, where the receiver omits communication on some control flow paths, in what is effectively a simplified version of the introductory example.</p>\n<pre><code>// Incoming error simulates an error produced internally.\nfunc EarlyReturn(err error) {\n    ch := make(chan any)\n    // Create a worker goroutine.\n    go func() {\n        // Send something to the channel.\n        // Leaks if the parent goroutine terminates early.\n        ch &lt;- struct{}{}\n    }()\n    if err != nil {\n        // The parent goroutine quits too early in case of an error.\n        // Sender leaks.\n        return\n    }\n    // Receive is only executed if there is no error.\n    &lt;-ch\n}</code></pre>\n<p>The goroutine leak is exposed by the profile:</p>\n<pre><code>ROUTINE ======================== main.EarlyReturn.func1 in .../main.go\n         0          1 (flat, cum)   100% of Total\n         .          .    140:   go func() {\n         .          1    143:           ch &lt;- struct{}{}\n         .          .    144:   }()\n         .          .    145:\n         .          .    146:   if err != nil {</code></pre>\n<p>The leak can be addressed by giving <code>ch</code> a buffer of size 1.</p>\n<h3 id=\"example-timeout\">Example: Timeout</h3>\n<p>A variation of the <strong>Early return</strong> pattern above involves contexts\nand non-deterministic choice (<code>select</code> statements):</p>\n<pre><code>func Timeout(ctx context.Context) {\n    // An unbuffered channel is used to coordinate\n    // a worker and parent thread\n    ch := make(chan any)\n    // Create worker goroutine\n    go func() {\n        // Perform some work then signal to the parent thread.\n        ch &lt;- struct{}{}\n    }()\n    // Wait for message from worker or context\n    // to be cancelled or timed out.\n    select {\n    case &lt;-ch: // Receive message from worker\n    case &lt;-ctx.Done():\n        // Sender leaks because there is no\n        // future rendezvous over the channel.\n    }\n}</code></pre>\n<p>If the context is cancelled before the sender synchronizes with the parent, the sender will leak:</p>\n<pre><code>(pprof) list Timeout\nTotal: 10\nROUTINE ======================== main.Timeout.func1.1 in .../main.go\n         0         10 (flat, cum)   100% of Total\n         .          .    198:           go func() {\n         .         10    201:                   ch &lt;- struct{}{}\n         .          .    202:           }()</code></pre>\n<p>As in the previous example, the fix is to give the channel buffer of size 1.</p>\n<h3 id=\"example-range-over-channel-without-closing\">Example: Range over channel without closing</h3>\n<p>Iterating over channels by using <code>range</code>\nallows you to repeatedly receive values from a channel in a loop.\nOnce the channel is closed and all values that have been enqueued\nin the channel’s buffer have been received, the loop exits.</p>\n<p>Importantly, <strong>if the channel is never closed</strong>, a <code>range</code> loop will block\nthe executing goroutine forever.\nOmitting the <code>close</code> operation is a common mistake, as below:</p>\n<pre><code>// Incoming list of items and the number of workers.\nfunc noCloseRange(list []any, workers int) {\n    // Create a channel that distributes work items.\n    ch := make(chan any)\n    // Create the worker goroutines.\n    for i := 0; i &lt; workers; i++ {\n        go func() {\n            // Each worker pulls items from the channel\n            // and then processes it.\n            for item := range ch {\n                // Process each item\n                _ = item\n            }\n        }()\n    }\n    // Queue items to the workers by using the channel.\n    for _, item := range list {\n        // The parent leaks by sending an item if workers == 0\n        // or if all the workers panic, but the panic is recovered.\n        ch &lt;- item\n    }\n    // Otherwise, the channel is never closed, so workers\n    // leak once there are no more items left to process.\n}\n...\ngo noCloseRange([]any{1, 2, 3}, 3) // Leaks all 3 workers</code></pre>\n<p>A goroutine leak profile for such a program would include the following:</p>\n<pre><code>Type: goroutineleak\n(pprof) list noCloseRange.func1\nTotal: 4\nROUTINE ======================== main.noCloseRange.func1 in .../main.go\n         0          3 (flat, cum) 75.00% of Total\n         .          .     82:           go func() {\n         .          3     84:                   for item := range ch {\n         .          .     86:                           _ = item\n         .          .     87:                   }\n         .          .     88:           }()</code></pre>\n<p>We see the 3 workers blocked at the <code>range ch</code> operation, which\ngives an ample hint as to the cause of the leak. The leak can be\naddressed by simply closing the channel once all messages have been sent:</p>\n<pre><code>    for _, item := range list {\n        ch &lt;- item\n    }\n    // All items have been sent. It is now safe to close.\n    close(ch)</code></pre>\n<p><strong>Bonus!</strong> Eagle-eyed readers may have spotted another potential\nleak in this example, if the number of workers is mistakenly set to zero,\nwhich will lead the parent sender to leak:</p>\n<pre><code>go noCloseRange([]any{1, 2, 3}, 0) // Sender leaks with 0 workers</code></pre>\n<p>This is also captured by the profile:</p>\n<pre><code>(pprof) list noCloseRange$\nTotal: 4\nROUTINE ======================== main.noCloseRange in .../main.go\n         0          1 (flat, cum) 25.00% of Total\n         .          .     76:func noCloseRange(list []any, workers int) {\n...\n         .          .     92:   for _, item := range list {\n         .          1     95:           ch &lt;- item\n         .          .     96:   }</code></pre>\n<p>While <code>workers &gt; 0</code> can be assumed to hold in realistic production systems,\ngoroutine leak profiles can nevertheless be used to implicitly monitor for off-chance\nviolations without conservative <code>workers &lt;= 0</code> checks.</p>\n<h3 id=\"example-method-contract-violations\">Example: Method contract violations</h3>\n<p>The patterns seen so far have been relatively constrained in their lexical scope. However, as functionality is spread out across functions, methods and packages, and implementations are obfuscated by interfaces, the difficulty of manually detecting leaks drastically increases.</p>\n<p>Such a case is exemplified in this section, with the custom <code>worker</code> type that embeds two channel\nfields, <code>ch</code> and <code>done</code> and creates a looping goroutine with its <code>Start</code> method that\nreads from both channels with a <code>select</code> statement.\nSaid goroutine can only be terminated by receiving a message through the <code>done</code> channel,\nwhich is closed by the <code>Stop</code> method.</p>\n<p>The <code>Start</code> method can be invoked any number of times, but if it is invoked\nat least once, <code>Stop</code> should eventually be called.</p>\n<p>As a result, <code>Start</code> and <code>Stop</code> form an implicit contract that dictates the order\nin which the methods should be invoked.\nBreaking that contract can lead to undesirable behavior,\nin this case, goroutine leaks:</p>\n<pre><code>func MethodContractViolation() {\n    items := make([]any, 10)\n    // Create a new worker\n    w := NewWorker()\n    // Start worker\n    w.Start()\n    // Operate on worker\n    for _, item := range items {\n        w.AddToQueue(item)\n    }\n    // Exits without calling ’Stop’.\n}\ntype worker struct {\n    ch   chan any\n    done chan any\n}\ntype Worker interface {\n    Start()\n    Stop()\n    AddToQueue(item any)\n}\nfunc NewWorker() Worker {\n    return &amp;worker{\n        ch:   make(chan any),\n        done: make(chan any),\n    }\n}\n// Start spawns a background goroutine that extracts items pushed to the queue.\nfunc (w *worker) Start() {\n    go func() {\n        for {\n            select {\n            case &lt;-w.ch: // Normal workflow\n            case &lt;-w.done:\n                return // Shut down\n            }\n        }\n    }()\n}\nfunc (w *worker) Stop() {\n    // Allows goroutine created by Start to terminate\n    close(w.done)\n}\nfunc (w *worker) AddToQueue(item any) {\n    w.ch &lt;- item\n}</code></pre>\n<p>This issue is further exacerbated in practice, where such custom types are only\nexported as interfaces, in this case, through the non-descript\n<code>Worker</code> type.\nClients may not even be aware of the underlying implementation and,\nconsequently, violate the implicit contract without realizing.</p>\n<p>Fortunately, soliciting a goroutine leak profile can reveal the defect:</p>\n<pre><code>(pprof) list Start\nTotal: 1\nROUTINE ======================== main.(*worker).Start.func1 in .../main.go\n         0          1 (flat, cum)   100% of Total\n         .          .    266:   go func() {\n         .          .    267:           for {\n         .          1    268:                   select {\n         .          .    269:                   case &lt;-w.ch:\n         .          .    270:                   case &lt;-w.done:\n         .          .    271:                           return</code></pre>\n<p>Naturally, the fix involves following the trail to the <code>Start</code> call\nand adding an invocation of <code>Stop</code>.</p>\n<h3 id=\"example-cockroach-missing-unlock\">Example (Cockroach): Missing unlock</h3>\n<p>The following example\nis taken from CockroachDB.\nIt involves acquiring and releasing a lock in a loop,\nbut forgetting to unlock it\nbefore executing a <code>break</code> statement:</p>\n<pre><code>type Gossip struct {\n    mu     sync.Mutex\n    closed bool\n}\nfunc (g *Gossip) bootstrap() {\n    for {\n        g.mu.Lock()\n        if g.closed {\n            // Missing g.mu.Unlock\n            break\n        }\n        g.mu.Unlock()\n    }\n}\nfunc Cockroach584() {\n    g := &amp;Gossip{\n        closed: true,\n    }\n    // ...\n    g.bootstrap()\n    g.bootstrap() // Causes a leak\n}</code></pre>\n<p>In such a case, the goroutine will leak when failing to acquire the lock.</p>\n<pre><code>(pprof) list Gossip\nTotal: 1\nROUTINE ======================== main.(*Gossip).bootstrap in .../main.go\n         0          1 (flat, cum)   100% of Total\n         .          .    165:func (g *Gossip) bootstrap() {\n         .          .    166:   for {\n         .          1    167:           g.mu.Lock()\n         .          .    168:           if g.closed {\n         .          .    170:                   break\n         .          .    171:           }\n         .          .    172:           g.mu.Unlock()</code></pre>\n<p>Adding a call to <code>Unlock</code> before the <code>break</code> addresses the issue.</p>\n<h3 id=\"example-etcd-unexpected-channel-operation-orderings\">Example (etcd): Unexpected channel operation orderings</h3>\n<p>This example, found in etcd, shows how an unexpected ordering between channel operations can lead to a goroutine leak:</p>\n<pre><code>type node struct {\n    status chan chan struct{}\n    stop   chan struct{}\n    done   chan struct{}\n}\nfunc (n *node) Status() struct{} {\n    c := make(chan struct{})\n    n.status &lt;- c\n    return &lt;-c\n}\nfunc (n *node) run() {\n    for {\n        select {\n        case c := &lt;-n.status:\n            c &lt;- struct{}{}\n        case &lt;-n.stop:\n            close(n.done)\n            return\n        }\n    }\n}\nfunc (n *node) Stop() {\n    select {\n    case n.stop &lt;- struct{}{}:\n    case &lt;-n.done:\n        return\n    }\n    &lt;-n.done\n}\nfunc Etcd6857() {\n    n := &amp;node{\n        status: make(chan chan struct{}),\n        stop:   make(chan struct{}),\n        done:   make(chan struct{}),\n    }\n    go n.run()\n    go n.Status()\n    go n.Stop()\n}</code></pre>\n<p>The <code>run</code> method fires a loop which expects to\nrepeatedly receive messages over the <code>status</code> channel\n(sent by invoking the <code>Status</code> method).\nAt the same time, it can also receive one message over the\n<code>stop</code> channel (sent via the <code>Stop</code> method),\nat which point it closes the <code>done</code> channel and exits.\nThe <code>Stop</code> method itself then waits to receive message\nover <code>done</code>, which is unblocked once <code>done</code> is closed.</p>\n<p>A leak may occur if the <code>run</code>, <code>Status</code>, and <code>Stop</code> methods\nrun concurrently.\nThe <code>Stop</code> and <code>run</code> goroutines can synchronize\nand exit without receiving the message issued\nby <code>Status</code>, causing it to block forever.</p>\n<pre><code>(pprof) list Status\nTotal: 8\nROUTINE ======================== main.(*node).Status in .../main.go\n         0          8 (flat, cum)   100% of Total\n         .          .     16:func (n *node) Status() struct{} {\n         .          .     17:   c := make(chan struct{})\n         .          8     18:   n.status &lt;- c\n         .          .     19:   return &lt;-c\n         .          .     20:}</code></pre>\n<p>Wrapping the send to <code>status</code> in a <code>select</code> statement\nwhere the other <code>case</code> branch tries to receive a message\nover <code>done</code> allows the goroutine running to <code>Status</code>\nto gracefully exit if it lost the race with a <code>Stop</code>\ncall.</p>\n<h3 id=\"example-kubernetes-mutual-blocking-between-channels-and-mutexes\">Example (Kubernetes): Mutual blocking between channels and mutexes</h3>\n<p>This example occurs in Kubernetes, as a result of mixing channels and locks:</p>\n<pre><code>type Connection struct {\n    closeChan chan bool\n}\ntype idleAwareFramer struct {\n    resetChan chan bool\n    writeLock sync.Mutex\n    conn      *Connection\n}\nfunc (i *idleAwareFramer) monitor() {\n    var resetChan = i.resetChan\n    for range i.conn.closeChan {\n        i.writeLock.Lock()\n        close(resetChan)\n        i.resetChan = nil\n        i.writeLock.Unlock()\n        break\n    }\n}\nfunc (i *idleAwareFramer) WriteFrame() {\n    i.writeLock.Lock()\n    defer i.writeLock.Unlock()\n    if i.resetChan == nil {\n        return\n    }\n    i.resetChan &lt;- true\n}\nfunc NewIdleAwareFramer() *idleAwareFramer {\n    return &amp;idleAwareFramer{\n        resetChan: make(chan bool),\n        conn: &amp;Connection{\n            closeChan: make(chan bool),\n        },\n    }\n}\nfunc Kubernetes6632() {\n    i := NewIdleAwareFramer()\n    go func() {\n        i.conn.closeChan &lt;- true\n    }()\n    go i.monitor()\n    go i.WriteFrame()\n}</code></pre>\n<p>The goroutine running <code>WriteFrame</code> may acquire the\nidle-aware framer lock, followed by sending a message over the\n<code>resetChan</code> channel, while the <code>monitor</code> goroutine\nwaits to receive a message over the <code>closeChan</code> channel.\nOnce a message has been dispatched, the <code>monitor</code> goroutine\nwill attempt to acquire the same lock.\nHowever, since there isn’t any traffic over <code>resetChan</code>, the send operation\nblocks forever, preventing the <code>monitor</code> goroutine from releasing\nthe lock.\nThis, in turn, causes both goroutines to leak.</p>\n<pre><code>(pprof) list AwareFramer\nTotal: 200\nROUTINE ======================== main.(*idleAwareFramer).WriteFrame in .../main.go\n         0        100 (flat, cum) 50.00% of Total\n         .          .     32:func (i *idleAwareFramer) WriteFrame() {\n         .          .     33:   i.writeLock.Lock()\n         .          .     34:   defer i.writeLock.Unlock()\n         .          .     35:   if i.resetChan == nil {\n         .          .     36:           return\n         .          .     37:   }\n         .        100     38:   i.resetChan &lt;- true\n         .          .     39:}\nROUTINE ======================== main.(*idleAwareFramer).monitor in .../main.go\n         0        100 (flat, cum) 50.00% of Total\n         .          .     21:func (i *idleAwareFramer) monitor() {\n         .          .     22:   var resetChan = i.resetChan\n         .          .     23:   for range i.conn.closeChan {\n         .        100     24:           i.writeLock.Lock()\n         .          .     25:           close(resetChan)</code></pre>\n<p>The fix is to set up a separate goroutine after a message is received\nover <code>closeChan</code> in the <code>monitor</code> goroutine that drains the <code>resetChan</code>\nbefore attempting to acquire the lock.</p>\n<h3 id=\"example-moby-misusing-sync-waitgroup\">Example (Moby): Misusing <code>sync.WaitGroup</code></h3>\n<p>The following example in Moby showcases how wait groups may cause leaks:</p>\n<pre><code>type Manager struct {\n    plugins []int\n}\nfunc (pm *Manager) init() {\n    var group sync.WaitGroup\n    group.Add(len(pm.plugins))\n    for _, p := range pm.plugins {\n        go func(p int) {\n            defer group.Done()\n        }(p)\n        group.Wait() // Block here\n    }\n}\nfunc Moby25384() {\n    pm := &amp;Manager{\n        plugins: []int{1, 2},\n    }\n    go pm.init()\n}</code></pre>\n<p>The <code>group</code> wait group increments its counter\ndepending on the number of plugins held by the\nplugin manager <code>pm</code>, then iterates over each plugin\nand spawns a goroutine.\nEach goroutine decrements the counter once it finishes\nits task with the <code>Done</code> method.\nHowever, <code>group</code> erroneously invokes <code>Wait</code> inside\nthe loop body, instead of after it!\nThis will cause any goroutine running the <code>init</code> method\nwhen the manager has more than one plugin to leak.</p>\n<pre><code>(pprof) list init\nTotal: 1\nROUTINE ======================== main.(*Manager).init in .../main.go\n         0          1 (flat, cum)   100% of Total\n         .          .     17:   group.Add(len(pm.plugins))\n         .          .     18:   for _, p := range pm.plugins {\n         .          .     19:           go func(p int) {\n         .          .     20:                   defer group.Done()\n         .          .     21:           }(p)\n         .          1     22:           group.Wait() // Block here\n         .          .     23:   }</code></pre>\n<p>This can be easily addressed by moving the <code>Wait</code> outside\nthe loop.</p>\n<h3 id=\"example-moby-mutual-blocking-between-channels-and-mutexes\">Example (Moby): Mutual blocking between channels and mutexes</h3>\n<p>Another example in Moby showcases a mixed channel-lock leak:</p>\n<pre><code>type (\n    State struct {\n        Health *Health\n    }\n    Container struct {\n        sync.Mutex\n        State *State\n    }\n    Store struct {\n        ctr *Container\n    }\n    Daemon struct {\n        containers Store\n    }\n    Health struct {\n        stop chan struct{}\n    }\n)\nfunc (d *Daemon) StateChanged() {\n    c := d.containers.ctr\n    c.Lock()\n    d.updateHealthMonitorElseBranch(c)\n    defer c.Unlock()\n}\nfunc (d *Daemon) updateHealthMonitorElseBranch(c *Container) {\n    c.State.Health.CloseMonitorChannel()\n}\nfunc (s *Health) CloseMonitorChannel() {\n    if s.stop != nil {\n        s.stop &lt;- struct{}{}\n    }\n}\nfunc monitor(c *Container, stop chan struct{}) {\n    for {\n        select {\n        case &lt;-stop:\n            return\n        default:\n            handleProbeResult(c)\n        }\n    }\n}\nfunc handleProbeResult(c *Container) {\n    c.Lock()\n    defer c.Unlock()\n    // Additional work...\n}\nfunc NewDaemonAndContainer() (*Daemon, *Container) {\n    c := &amp;Container{\n        State: &amp;State{&amp;Health{\n            stop: make(chan struct{}),\n        }},\n    }\n    d := &amp;Daemon{Store{c}}\n    return d, c\n}\nfunc Moby28462() {\n    d, c := NewDaemonAndContainer()\n    go monitor(c, c.State.Health.stop)\n    go d.StateChanged()\n}</code></pre>\n<p>The goroutine invoking <code>StateChanged</code> may acquire the lock\nof the container stored by the daemon, then invoke\nthe <code>updateHealthMonitorElseBranch</code> method on\nthe daemon, which attempts to send a message over\nthe <code>stop</code> channel of the container.\nHowever, the goroutine running <code>monitor</code>\nmay fail to receive a message over <code>stop</code>, if the message\nis not already in-flight, and instead unblock by picking\nthe <code>default</code> case of the <code>select</code> statement.\nThis will lead it to try to acquire the same container\nlock that is already held by the <code>StateChanged</code>\ngoroutine, leading both goroutines to leak.</p>\n<pre><code>(pprof) list .CloseMonitorChannel\nTotal: 2\nROUTINE ======================== main.(*Health).CloseMonitorChannel in .../main.go\n         0          1 (flat, cum) 50.00% of Total\n         .          .     66:func (s *Health) CloseMonitorChannel() {\n         .          .     67:   if s.stop != nil {\n         .          1     68:           s.stop &lt;- struct{}{}\n         .          .     69:   }\n         .          .     70:}\n(pprof) list main.handleProbeResult\nTotal: 2\nROUTINE ======================== main.handleProbeResult in .../main.go\n         0          1 (flat, cum) 50.00% of Total\n         .          .     83:func handleProbeResult(c *Container) {\n         .          1     84:   c.Lock()\n         .          .     85:   // Additional work...\n         .          .     86:   defer c.Unlock()\n         .          .     87:}</code></pre>\n<p>The fix is to close the <code>stop</code> channel instead\nof sending a message over it.\nSince closing a channel is not a blocking operation,\nthe <code>StateChanged</code> goroutine is then able to release\nthe lock.\nIn turn, this unblocks the <code>monitor</code> goroutine,\nwhich may now terminate by picking unblocked\n<code>&lt;-stop</code> case branch in the <code>select</code> statement\non the next loop iteration.</p>\n<pre><code>        **Next article:** Size-Specialized Memory Allocation\n\n      \n    \n    \n      \n        **Previous article:** Generic Methods\n\n      \n    \n    **Blog Index**</code></pre>","headings":[{"level":1,"text":"Goroutine Leak Profiles","id":"goroutine-leak-profiles"},{"level":2,"text":"Example: concurrent workers","id":"example-concurrent-workers"},{"level":3,"text":"Debugging with the goroutine leak profiler","id":"debugging-with-the-goroutine-leak-profiler"},{"level":3,"text":"Collecting the profile","id":"collecting-the-profile"},{"level":3,"text":"Addressing the leak","id":"addressing-the-leak"},{"level":2,"text":"Implementation","id":"implementation"},{"level":3,"text":"Core concept","id":"core-concept"},{"level":3,"text":"Limitations","id":"limitations"},{"level":3,"text":"Performance impact","id":"performance-impact"},{"level":2,"text":"Acknowledgements","id":"acknowledgements"},{"level":2,"text":"Additional examples","id":"additional-examples"},{"level":3,"text":"Example: Double send","id":"example-double-send"},{"level":3,"text":"Example: Early return","id":"example-early-return"},{"level":3,"text":"Example: Timeout","id":"example-timeout"},{"level":3,"text":"Example: Range over channel without closing","id":"example-range-over-channel-without-closing"},{"level":3,"text":"Example: Method contract violations","id":"example-method-contract-violations"},{"level":3,"text":"Example (Cockroach): Missing unlock","id":"example-cockroach-missing-unlock"},{"level":3,"text":"Example (etcd): Unexpected channel operation orderings","id":"example-etcd-unexpected-channel-operation-orderings"},{"level":3,"text":"Example (Kubernetes): Mutual blocking between channels and mutexes","id":"example-kubernetes-mutual-blocking-between-channels-and-mutexes"},{"level":3,"text":"Example (Moby): Misusing sync.WaitGroup","id":"example-moby-misusing-sync-waitgroup"},{"level":3,"text":"Example (Moby): Mutual blocking between channels and mutexes","id":"example-moby-mutual-blocking-between-channels-and-mutexes"}]}}