← Files GophersARCHIVED FILE
skills/go-concurrency/references/leaks-and-synctest.md
3.63 KB · Oct 3, 2026 · 06:31 UTC
# Leak Detection and Deterministic Time
Two tools cover the long tail of concurrency bugs: `go.uber.org/goleak` for unit tests, and `testing/synctest` for time-dependent code. A third — Go 1.26's experimental `goroutineleak` pprof profile — helps in production.
## goleak in tests
The cheapest way to catch leaks is to wire `goleak` into every package that spawns goroutines.
### Whole-package guard
```go
package worker_test
import (
"testing"
"go.uber.org/goleak"
)
func TestMain(m *testing.M) { goleak.VerifyTestMain(m) }
```
`VerifyTestMain` runs the tests, then checks that no extra goroutines remain. If a test forgot to cancel a context or close a channel, the run fails with the offending stack.
### Per-test guard
```go
func TestWorker(t *testing.T) {
defer goleak.VerifyNone(t)
w := newWorker()
w.Start()
w.Stop()
}
```
Per-test is finer-grained but you must remember the `defer` in every test.
### Allow-listing noise
Some libraries (telemetry exporters, drivers) keep background goroutines that are not leaks. Allow them explicitly:
```go
goleak.VerifyTestMain(m,
goleak.IgnoreTopFunction("go.opencensus.io/stats/view.(*worker).start"),
goleak.IgnoreCurrent(),
)
```
Avoid `IgnoreAnyFunction` — it suppresses too much.
## testing/synctest
`testing/synctest` (stable Go 1.25, extended in 1.26) gives deterministic time. Synthetic time only advances when **every** goroutine in the bubble is blocked, so timing-dependent tests stop being flaky.
```go
import "testing/synctest"
func TestTimeout(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
defer cancel()
time.Sleep(5 * time.Second) // synthetic
synctest.Wait() // drain ready goroutines
if !errors.Is(ctx.Err(), context.DeadlineExceeded) {
t.Fatalf("got %v, want DeadlineExceeded", ctx.Err())
}
})
}
```
Inside the bubble:
- `time.Sleep`, `time.After`, `time.Ticker` use the synthetic clock.
- `synctest.Wait` blocks until every goroutine in the bubble is blocked.
- All goroutines started inside the bubble must finish before the test returns.
Use `synctest.Test` (Go 1.25+); only use the Go 1.24 experimental `synctest.Run` if the module is pinned to 1.24 and opts in with `GOEXPERIMENT=synctest`.
### Good fits
- Timeout and deadline behaviour
- Retry/backoff loops
- Token-bucket rate limiters
- `select` with timers
### Not a fit
- Tests that depend on real wall-clock latency (network, fsync)
- Tests that need to observe scheduling jitter
## Go 1.26 experimental goroutineleak profile
For production-side investigation, Go 1.26 adds a `goroutineleak` pprof profile behind `GOEXPERIMENT=goroutineleakprofile`:
```bash
go build -tags=... ./... # build with the experiment
GOEXPERIMENT=goroutineleakprofile ./service
# in another shell
curl http://localhost:6060/debug/pprof/goroutineleak?debug=2
go tool pprof http://localhost:6060/debug/pprof/goroutineleak
```
This is **not** a substitute for `goleak` in tests — it is for diagnosing live services where goroutine count grows over time. Keep all the existing tools too:
- `go test -race ./...` — race detector
- `runtime.NumGoroutine()` — quick runtime count
- `/debug/pprof/goroutine?debug=2` — full stack dump
## Debugging checklist
When a leak is suspected:
1. Run `go test -race ./...`.
2. Add `defer goleak.VerifyNone(t)` to the suspected test; the failure shows the leaked stack.
3. If reproduction needs timing, port the test to `synctest.Test`.
4. In production, scrape `/debug/pprof/goroutine?debug=2` before and after the workload — diff the counts.
SHA-256: 2981a8b5744da80dad984b121baff0624f13e101aabe6456779afcfdfcc9f0f6