The gleak package provides goroutine leak detection for Go tests. It helps identify goroutines that are created during tests but not properly cleaned up, which can lead to resource leaks and test interference.
Goroutine leaks are a common problem in concurrent Go programs. The gleak package helps detect these leaks by:
{ .api }
func IgnoreCurrent() IgnoredGoroutinesReturns a set of currently running goroutines to ignore in leak detection. This is typically called at the beginning of a test to establish a baseline.
Returns: An IgnoredGoroutines filter containing all currently running goroutines
Usage:
// Capture baseline at test start
ignored := gleak.IgnoreCurrent()
// Run test code that may create goroutines
doSomethingConcurrent()
// Check for leaks, ignoring the baseline
Eventually(gleak.Goroutines).ShouldNot(gleak.HaveLeaked(ignored)){ .api }
func IgnoreBackgroundGoroutines() IgnoredGoroutinesReturns a set of known background goroutines to ignore. This includes common system goroutines created by the Go runtime, testing framework, and other standard libraries.
Returns: An IgnoredGoroutines filter containing known background goroutines
Usage:
// Ignore standard background goroutines
Eventually(gleak.Goroutines).ShouldNot(
gleak.HaveLeaked(gleak.IgnoreBackgroundGoroutines()),
)Common Background Goroutines Ignored:
{ .api }
type IgnoredGoroutines interface {
// Methods to filter goroutines
}An interface representing a set of goroutines to ignore during leak detection. This is an opaque type used to configure the HaveLeaked matcher.
{ .api }
func HaveLeaked(options ...IgnoredGoroutines) types.GomegaMatcherCreates a matcher that fails if goroutines have leaked. The matcher compares current goroutines against the ignored set and reports any unexpected goroutines.
Parameters:
options - Zero or more IgnoredGoroutines filters to applyReturns: A Gomega matcher
Usage:
// Basic leak detection
Expect(gleak.Goroutines()).ShouldNot(gleak.HaveLeaked())
// With ignored goroutines
ignored := gleak.IgnoreCurrent()
Expect(gleak.Goroutines()).ShouldNot(gleak.HaveLeaked(ignored))
// Multiple ignore filters
Expect(gleak.Goroutines()).ShouldNot(
gleak.HaveLeaked(
gleak.IgnoreCurrent(),
gleak.IgnoreBackgroundGoroutines(),
),
)The gleak/goroutine sub-package provides utilities for inspecting goroutines.
{ .api }
type Goroutine struct {
ID uint64
State string
Stack string
// Additional fields
}Represents information about a single goroutine.
Fields:
ID - Unique goroutine IDState - Current state (e.g., "running", "waiting", "syscall")Stack - Stack trace of the goroutine{ .api }
func All() []GoroutineReturns information about all currently running goroutines.
Returns: A slice of Goroutine objects
Example:
import "github.com/onsi/gomega/gleak/goroutine"
goroutines := goroutine.All()
for _, g := range goroutines {
fmt.Printf("Goroutine %d: %s\n", g.ID, g.State)
fmt.Printf("Stack:\n%s\n", g.Stack)
}{ .api }
func Current() GoroutineReturns information about the current goroutine.
Returns: A Goroutine object
Example:
current := goroutine.Current()
fmt.Printf("Current goroutine ID: %d\n", current.ID)package mypackage_test
import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/onsi/gomega/gleak"
)
var _ = Describe("Goroutine Leak Detection", func() {
It("detects leaked goroutines", func() {
// Capture baseline
good := gleak.IgnoreCurrent()
// Create a goroutine that will leak
ch := make(chan bool)
go func() {
<-ch // This will block forever
}()
// This will fail because of the leaked goroutine
Eventually(gleak.Goroutines).ShouldNot(gleak.HaveLeaked(good))
})
})var _ = Describe("Worker Pool", func() {
It("cleans up all worker goroutines", func() {
// Establish baseline
good := gleak.IgnoreCurrent()
// Create worker pool
ctx, cancel := context.WithCancel(context.Background())
workers := 10
for i := 0; i < workers; i++ {
go func() {
for {
select {
case <-ctx.Done():
return // Proper cleanup
case work := <-workQueue:
processWork(work)
}
}
}()
}
// Do some work
for i := 0; i < 100; i++ {
workQueue <- i
}
// Clean up
cancel()
// Verify no leaks
Eventually(gleak.Goroutines).ShouldNot(gleak.HaveLeaked(good))
})
})var _ = Describe("HTTP Server", func() {
var server *http.Server
var good gleak.IgnoredGoroutines
BeforeEach(func() {
// Capture baseline before each test
good = gleak.IgnoreCurrent()
// Create server
server = &http.Server{
Addr: ":8080",
Handler: http.HandlerFunc(handler),
}
// Start server in background
go func() {
server.ListenAndServe()
}()
// Wait for server to be ready
Eventually(func() error {
_, err := http.Get("http://localhost:8080/health")
return err
}).Should(Succeed())
})
AfterEach(func() {
// Shutdown server gracefully
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
err := server.Shutdown(ctx)
Expect(err).NotTo(HaveOccurred())
// Verify server goroutines are cleaned up
Eventually(gleak.Goroutines).ShouldNot(gleak.HaveLeaked(good))
})
It("handles requests", func() {
resp, err := http.Get("http://localhost:8080/api/data")
Expect(err).NotTo(HaveOccurred())
Expect(resp.StatusCode).To(Equal(200))
})
})var _ = Describe("Channel Operations", func() {
It("ensures channels are properly closed", func() {
good := gleak.IgnoreCurrent()
// Create channels
dataCh := make(chan int)
doneCh := make(chan bool)
// Start producer
go func() {
defer close(dataCh)
for i := 0; i < 100; i++ {
dataCh <- i
}
}()
// Start consumer
go func() {
defer close(doneCh)
for range dataCh {
// Process data
}
}()
// Wait for completion
Eventually(doneCh).Should(BeClosed())
// Verify no leaks
Eventually(gleak.Goroutines).ShouldNot(gleak.HaveLeaked(good))
})
})var _ = Describe("Complex Scenario", func() {
It("combines multiple ignore filters", func() {
// Ignore both current state and known background goroutines
Eventually(gleak.Goroutines).ShouldNot(
gleak.HaveLeaked(
gleak.IgnoreCurrent(),
gleak.IgnoreBackgroundGoroutines(),
),
)
// Run test logic
result := performComplexOperation()
Expect(result).To(Succeed())
// Re-check with same filters
Eventually(gleak.Goroutines).ShouldNot(
gleak.HaveLeaked(
gleak.IgnoreCurrent(),
gleak.IgnoreBackgroundGoroutines(),
),
)
})
})var _ = Describe("Async Shutdown", func() {
It("waits for goroutines to complete", func() {
good := gleak.IgnoreCurrent()
// Start background work
done := make(chan bool)
go func() {
defer close(done)
// Simulate work that takes time
time.Sleep(500 * time.Millisecond)
}()
// Signal shutdown
// ... shutdown logic ...
// Use Eventually to wait for cleanup
Eventually(done, "2s").Should(BeClosed())
Eventually(gleak.Goroutines).ShouldNot(gleak.HaveLeaked(good))
})
})var _ = Describe("Debug Leaks", func() {
It("provides information about leaked goroutines", func() {
good := gleak.IgnoreCurrent()
// Create a leak
ch := make(chan int)
go func() {
<-ch // Blocks forever
}()
// Get current goroutines
current := goroutine.All()
// Filter out ignored ones (manual comparison for debugging)
fmt.Println("\nAll goroutines:")
for _, g := range current {
fmt.Printf("\nGoroutine %d [%s]:\n%s\n", g.ID, g.State, g.Stack)
}
// This will fail and show leak details
Eventually(gleak.Goroutines).ShouldNot(gleak.HaveLeaked(good))
})
})var _ = Describe("Service Tests", func() {
var baseline gleak.IgnoredGoroutines
BeforeEach(func() {
// Set baseline before each test
baseline = gleak.IgnoreCurrent()
})
AfterEach(func() {
// Check for leaks after each test
Eventually(gleak.Goroutines).ShouldNot(
gleak.HaveLeaked(baseline),
"Test should not leak goroutines",
)
})
Context("with background workers", func() {
var ctx context.Context
var cancel context.CancelFunc
BeforeEach(func() {
ctx, cancel = context.WithCancel(context.Background())
// Start workers
for i := 0; i < 5; i++ {
go worker(ctx)
}
})
AfterEach(func() {
// Clean up workers
cancel()
// Give workers time to exit
time.Sleep(100 * time.Millisecond)
})
It("processes jobs without leaking", func() {
// Test logic
submitJobs(100)
// Leak check happens automatically in AfterEach
})
})
})var _ = Describe("Ticker Cleanup", func() {
It("stops tickers properly", func() {
good := gleak.IgnoreCurrent()
ticker := time.NewTicker(100 * time.Millisecond)
done := make(chan bool)
go func() {
defer close(done)
for {
select {
case <-ticker.C:
// Do periodic work
doPeriodicWork()
case <-done:
return
}
}
}()
// Run for a bit
time.Sleep(500 * time.Millisecond)
// Clean up
ticker.Stop()
close(done)
// Verify cleanup
Eventually(gleak.Goroutines).ShouldNot(gleak.HaveLeaked(good))
})
})var _ = Describe("Context Pattern", func() {
It("uses context for proper cleanup", func() {
good := gleak.IgnoreCurrent()
ctx, cancel := context.WithTimeout(
context.Background(),
2*time.Second,
)
defer cancel()
results := make(chan Result)
// Start multiple workers
for i := 0; i < 10; i++ {
go func(id int) {
for {
select {
case <-ctx.Done():
return
default:
result := processItem(id)
select {
case results <- result:
case <-ctx.Done():
return
}
}
}
}(i)
}
// Collect some results
for i := 0; i < 50; i++ {
<-results
}
// Cancel context
cancel()
// All workers should exit
Eventually(gleak.Goroutines).ShouldNot(gleak.HaveLeaked(good))
})
})Always capture the baseline at the start of your test:
It("test case", func() {
// First thing: establish baseline
good := gleak.IgnoreCurrent()
// Then run test logic
doTestLogic()
// Finally check for leaks
Eventually(gleak.Goroutines).ShouldNot(gleak.HaveLeaked(good))
})Goroutines may take time to exit, so use Eventually:
// Good: Gives goroutines time to clean up
Eventually(gleak.Goroutines).ShouldNot(gleak.HaveLeaked(good))
// Avoid: May fail if goroutines haven't exited yet
Expect(gleak.Goroutines()).ShouldNot(gleak.HaveLeaked(good))Ensure all resources are properly cleaned up:
// Use defer for cleanup
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Close channels when done
defer close(ch)
// Stop tickers
defer ticker.Stop()
// Shutdown servers
defer server.Shutdown(context.Background())Prefer context-based cancellation for clean shutdown:
// Good: Context-based cancellation
ctx, cancel := context.WithCancel(context.Background())
go func() {
for {
select {
case <-ctx.Done():
return
default:
doWork()
}
}
}()
// Later: cancel()
// Avoid: No clean way to stop
go func() {
for {
doWork() // Runs forever
}
}()Centralize leak detection in AfterEach:
var _ = Describe("Suite", func() {
var baseline gleak.IgnoredGoroutines
BeforeEach(func() {
baseline = gleak.IgnoreCurrent()
})
AfterEach(func() {
Eventually(gleak.Goroutines).ShouldNot(
gleak.HaveLeaked(baseline),
)
})
// All tests automatically checked
It("test 1", func() { ... })
It("test 2", func() { ... })
})Check both completion and leaks:
It("cleans up properly", func() {
good := gleak.IgnoreCurrent()
done := make(chan bool)
go func() {
defer close(done)
doWork()
}()
// Verify work completed
Eventually(done).Should(BeClosed())
// Verify no leaks
Eventually(gleak.Goroutines).ShouldNot(gleak.HaveLeaked(good))
})// Leak: Goroutine blocks forever
ch := make(chan int)
go func() {
ch <- 1 // No receiver, blocks forever
}()
// Fix: Use buffered channel or ensure receiver exists
ch := make(chan int, 1)
go func() {
ch <- 1 // Won't block
}()// Leak: No way to stop the goroutine
go func() {
for {
doWork()
}
}()
// Fix: Use context for cancellation
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
for {
select {
case <-ctx.Done():
return
default:
doWork()
}
}
}()// Leak: Ticker keeps goroutine alive
ticker := time.NewTicker(1 * time.Second)
go func() {
for range ticker.C {
doWork()
}
}()
// Fix: Stop ticker
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
done := make(chan bool)
go func() {
for {
select {
case <-ticker.C:
doWork()
case <-done:
return
}
}
}()// Leak: Server goroutines not cleaned up
server.ListenAndServe()
// Fix: Graceful shutdown
go server.ListenAndServe()
// Later:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
server.Shutdown(ctx)var _ = Describe("Suite with automatic leak detection", func() {
var baseline gleak.IgnoredGoroutines
BeforeEach(func() {
baseline = gleak.IgnoreCurrent()
})
AfterEach(func() {
Eventually(gleak.Goroutines, "5s", "100ms").ShouldNot(
gleak.HaveLeaked(baseline),
)
})
// All subsequent tests automatically get leak detection
})AfterEach(func() {
// Give more time for complex cleanup
Eventually(gleak.Goroutines, "10s", "500ms").ShouldNot(
gleak.HaveLeaked(baseline),
"All goroutines should exit within 10 seconds",
)
})