{
"$type": "site.standard.document",
"canonicalUrl": "https://rednafi.com/go/early-return-and-goroutine-leak/",
"description": "Prevent goroutine leaks caused by early returns with unbuffered channels. Learn buffering, draining, errgroup patterns, and goleak testing.",
"path": "/go/early-return-and-goroutine-leak/",
"publishedAt": "2025-09-07T00:00:00.000Z",
"site": "at://did:plc:fgtm2c26vfcj74rfmeggbyqj/site.standard.publication/3mnl6f7ob462z",
"tags": [
"Go",
"Concurrency",
"Testing"
],
"textContent": "At work, a common mistake I notice when reviewing candidates' home assignments is how they\nwire goroutines to channels and then return early.\n\nThe pattern usually looks like this:\n\n- start a few goroutines\n- each goroutine sends a result to its own unbuffered channel\n- in the main goroutine, read from those channels one by one\n- if any read contains an error, return early\n\nThe trap is the early return. With an unbuffered channel, a send blocks until a receiver is\nready. If you return before reading from the remaining channels, the goroutines writing to\nthem block forever. That's a goroutine leak.\n\nHere's how the bug appears in a tiny example: one worker intentionally fails, causing the\nmain goroutine to bail early. That early return skips the receive from ch2, leaving the\nsender on ch2 stuck.\n\nOne simple fix is to make sure you always read from both channels before you decide what to\ndo. This guarantees that every send has a matching receive and no goroutine gets stuck:\n\nThis is safe but it means you always wait for both workers even when the first one already\nfailed and the second result is irrelevant. If you want to return early without leaking,\nanother option is to use buffered channels so the producers don't block on send. A buffer of\nsize one is enough for this pattern.\n\nBuffered channels remove the blocked send, but they also make it easier to forget that a\nsecond result exists at all. If that second value carries data you must process, you should\nstill receive it. If it is truly fire and forget, buffering is fine.\n\nOften the cleanest approach is to drop the channel plumbing when you only need to run tasks\nand aggregate errors. The [errgroup] package lets each goroutine return an error while the\ngroup does the waiting. There is nothing to forget to receive, so there is nothing to leak.\n\nSometimes you also want peers to stop once one task fails. errgroup.WithContext gives you\na context that gets canceled as soon as any task returns an error. You pass that context\ninto your workers and have them check ctx.Done() so they can exit quickly.\n\nAt this point it is natural to ask if tools can catch the original bug for you. go vet\ncannot. Vet is static analysis that runs at build time. Whether a send blocks depends on\nruntime control flow and timing. Vet cannot prove that the function returns before a\nparticular receive in a general way, so it doesn't flag this pattern.\n\ngo test -race cannot either. The race detector detects unsynchronized concurrent memory\naccess. A goroutine stuck on a channel send isn't a data race. You may see a test hang until\ntimeout, but the tool won't point to a leaking goroutine.\n\nYou can turn this into a failing test with [goleak] from Uber. goleak fails if goroutines\nare still alive when a test ends. It snapshots all goroutines via the runtime, filters out\nthe standard background ones, and reports the rest. Wire it into a test that triggers the\nearly return and you will see the blocked sender's stack in the output.\n\nHere is a test that leaks and fails:\n\nThis test fails and prints the goroutine stack stuck in the send to ch2.\n\nIf you switch the implementation to a fixed version, the test passes. For example, the\ndraining fix:\n\nIf you prefer suite wide enforcement, add goleak to your TestMain. This way your entire\ntest run fails if any test leaks goroutines.\n\nIf you start goroutines that send on channels, think carefully about early returns. An\nunbuffered send waits for a receive, and if you return before that receive happens, you've\nleaked a goroutine.\n\nYou can avoid this by:\n\n- always draining all channels\n- buffering intentionally so sends don't block\n- or using errgroup, with or without context, so tasks return errors and cooperate on\n cancellation\n\nAdd goleak to your tests so leaks surface early during development.\n\n\n\n\n[errgroup]:\n pkg.go.dev/golang.org/x/sync/errgroup\n\n[goleak]:\n github.com/uber-go/goleak",
"title": "Early return and goroutine leak"
}