Package: github.com/onsi/gomega/gleak
The gleak package provides goroutine leak detection capabilities for Gomega. It helps identify goroutines that remain running after tests complete, which can indicate resource leaks or improper cleanup.
import "github.com/onsi/gomega/gleak"func Goroutines() []GoroutineReturns information about all current goroutines, including their IDs, states, top functions, creators, and backtraces.
Usage Example:
import (
. "github.com/onsi/gomega"
"github.com/onsi/gomega/gleak"
)
// Take a snapshot of current goroutines
snapshot := gleak.Goroutines()
// Later, check for leaks
Eventually(gleak.Goroutines).ShouldNot(gleak.HaveLeaked(snapshot))func Current() GoroutineReturns information about the current goroutine (the goroutine calling this function), including its ID, state, top function, creator, and backtrace.
Usage Example:
import (
. "github.com/onsi/gomega"
"github.com/onsi/gomega/gleak"
)
// Get current goroutine info
current := gleak.Current()
fmt.Printf("Current goroutine ID: %d\n", current.ID)
fmt.Printf("Top function: %s\n", current.TopFunction)func G(actual any, matchername string) (Goroutine, error)Helper function that extracts a Goroutine from the actual value passed to a matcher. This is used internally by gleak matchers but can be useful when writing custom goroutine matchers.
Parameters:
actual - The value passed to the matcher (typically a Goroutine or []Goroutine)matchername - The name of the matcher calling this function (for error messages)Returns:
Usage Example:
// Custom matcher example
func MyCustomGoroutineMatcher() types.GomegaMatcher {
return gcustom.MakeMatcher(func(actual any) (bool, error) {
g, err := gleak.G(actual, "MyCustomGoroutineMatcher")
if err != nil {
return false, err
}
// Use goroutine g for matching logic
return g.TopFunction == "mypackage.myFunction", nil
})
}func IgnoreGinkgoParallelClient()Configures gleak to ignore Ginkgo parallel client goroutines. This should be called once during test suite setup when using Ginkgo in parallel mode to prevent false positives from Ginkgo's internal goroutines.
This function adds a standard filter to ignore goroutines created by Ginkgo's parallel test execution framework.
Usage Example:
import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/onsi/gomega/gleak"
)
var _ = BeforeSuite(func() {
// Configure gleak for parallel Ginkgo tests
gleak.IgnoreGinkgoParallelClient()
})
var _ = Describe("MyTests", func() {
It("detects leaks without false positives from Ginkgo", func() {
Eventually(gleak.Goroutines).ShouldNot(gleak.HaveLeaked())
})
})func HaveLeaked(ignoring ...any) types.GomegaMatcherReturns a matcher that succeeds if goroutines remain after filtering out expected goroutines. The matcher automatically filters well-known runtime and testing goroutines using built-in standard filters.
Parameters:
ignoring - Optional list of goroutines or filters to ignore. Can be:
string - Top function name (shorthand for IgnoringTopFunction)[]Goroutine - Slice of goroutines to ignore (shorthand for IgnoringGoroutines)types.GomegaMatcher - Any goroutine filter matcherUsage Examples:
import (
"time"
. "github.com/onsi/gomega"
"github.com/onsi/gomega/gleak"
)
// Basic leak detection (using Eventually, not Expect)
Eventually(gleak.Goroutines).ShouldNot(gleak.HaveLeaked())
// With custom timeout and polling
Eventually(gleak.Goroutines).
WithTimeout(5 * time.Second).
WithPolling(100 * time.Millisecond).
ShouldNot(gleak.HaveLeaked())
// Ignore specific top function by name
Eventually(gleak.Goroutines).ShouldNot(gleak.HaveLeaked("foo.bar"))
// Ignore multiple functions
Eventually(gleak.Goroutines).ShouldNot(gleak.HaveLeaked(
"foo.bar",
"baz.qux",
))
// Use snapshot-based detection
snapshot := gleak.Goroutines()
DoSomething()
Eventually(gleak.Goroutines).ShouldNot(gleak.HaveLeaked(snapshot))
// Combine multiple filter types
Eventually(gleak.Goroutines).ShouldNot(gleak.HaveLeaked(
snapshot,
"mypackage.backgroundWorker",
gleak.IgnoringInBacktrace("runtime.ensureSigM"),
))Best Practice:
Always use Eventually with HaveLeaked, not Expect. This allows "pending" goroutines time to properly wind down:
// Correct - allows time for goroutines to terminate
Eventually(gleak.Goroutines).ShouldNot(gleak.HaveLeaked())
// Incorrect - may report false positives
Expect(gleak.Goroutines()).NotTo(gleak.HaveLeaked())Filter matchers identify goroutines that should be ignored during leak detection. Each filter can be used standalone or combined with HaveLeaked.
func IgnoringTopFunction(topfname string) types.GomegaMatcherMatches goroutines where the topmost function in the backtrace has the specified name.
Format Options:
"topfunction-name" - Exact match"topfunction-name..." - Prefix match (one level deeper)"topfunction-name [state]" - Exact match with state requirementUsage Examples:
import (
. "github.com/onsi/gomega"
"github.com/onsi/gomega/gleak"
)
// Ignore exact top function name
Eventually(gleak.Goroutines).ShouldNot(
gleak.HaveLeaked(gleak.IgnoringTopFunction("foo.bar")),
)
// Ignore with prefix match (matches "foo.bar.baz" but not "foo.bar")
Eventually(gleak.Goroutines).ShouldNot(
gleak.HaveLeaked(gleak.IgnoringTopFunction("foo.bar...")),
)
// Ignore only when in specific state
Eventually(gleak.Goroutines).ShouldNot(
gleak.HaveLeaked(gleak.IgnoringTopFunction("foo.bar [running]")),
)
// Ignore channel operations
Eventually(gleak.Goroutines).ShouldNot(
gleak.HaveLeaked(gleak.IgnoringTopFunction("foo.bar [chan receive]")),
)func IgnoringGoroutines(goroutines []Goroutine) types.GomegaMatcherMatches goroutines by their unique IDs. Typically used with snapshots taken before a test to filter out pre-existing goroutines.
Usage Example:
import (
. "github.com/onsi/gomega"
"github.com/onsi/gomega/gleak"
)
// Take snapshot before test
snapshot := gleak.Goroutines()
// Run code that may create goroutines
server := StartServer()
defer server.Stop()
// Verify no leaks (ignoring snapshot goroutines)
Eventually(gleak.Goroutines).ShouldNot(
gleak.HaveLeaked(gleak.IgnoringGoroutines(snapshot)),
)
// Shorthand form (same as above)
Eventually(gleak.Goroutines).ShouldNot(gleak.HaveLeaked(snapshot))func IgnoringInBacktrace(substring string) types.GomegaMatcherMatches goroutines where the specified substring appears anywhere in the backtrace.
Usage Example:
import (
. "github.com/onsi/gomega"
"github.com/onsi/gomega/gleak"
)
// Ignore goroutines with specific function in backtrace
Eventually(gleak.Goroutines).ShouldNot(
gleak.HaveLeaked(gleak.IgnoringInBacktrace("runtime.ensureSigM")),
)
// Ignore goroutines from specific package
Eventually(gleak.Goroutines).ShouldNot(
gleak.HaveLeaked(gleak.IgnoringInBacktrace("myapp/internal/worker")),
)func IgnoringCreator(creatorfname string) types.GomegaMatcherMatches goroutines created by a function with the specified name.
Format Options:
"creatorfunction-name" - Exact match"creatorfunction-name..." - Prefix match (one level deeper)Usage Examples:
import (
. "github.com/onsi/gomega"
"github.com/onsi/gomega/gleak"
)
// Ignore goroutines created by specific function
Eventually(gleak.Goroutines).ShouldNot(
gleak.HaveLeaked(gleak.IgnoringCreator("foo.StartWorker")),
)
// Ignore with prefix match (matches "foo.bar.baz" but not "foo.bar")
Eventually(gleak.Goroutines).ShouldNot(
gleak.HaveLeaked(gleak.IgnoringCreator("foo.bar...")),
)
// Useful for framework goroutines
Eventually(gleak.Goroutines).ShouldNot(
gleak.HaveLeaked(
gleak.IgnoringCreator("github.com/onsi/ginkgo/v2/internal.(*Suite).runNode"),
),
)type Goroutine struct {
ID uint64 // unique goroutine ID
State string // goroutine state, such as "running", "chan receive", etc.
TopFunction string // topmost function on goroutine's stack
CreatorFunction string // name of function creating this goroutine, if any
BornAt string // location where goroutine was started; format "file-path:line-number"
Backtrace string // goroutine's full backtrace (stack trace)
}Represents information about a single goroutine.
Goroutine IDs:
State Values:
Common state strings (without brackets):
"idle" - Idle goroutine"runnable" - Ready to run"running" - Currently executing"syscall" - In system call"chan receive" - Blocked on channel receive"chan send" - Blocked on channel send"select" - Blocked in select statement"sleep" - In time.Sleep"copystack" - Stack is being copied"preempted" - Preempted goroutineState may include additional information:
"(scan)" - Goroutine is being scanned by GC"X minutes" - Blocked for X minutes or more"locked to thread" - Locked to OS threadUsage Example:
import (
"fmt"
"github.com/onsi/gomega/gleak"
)
// Inspect current goroutines
goroutines := gleak.Goroutines()
for _, g := range goroutines {
fmt.Printf("Goroutine %d [%s]: %s\n", g.ID, g.State, g.TopFunction)
if g.CreatorFunction != "" {
fmt.Printf(" Created by: %s at %s\n", g.CreatorFunction, g.BornAt)
}
}Methods:
func (g Goroutine) String() stringReturns a short textual description without the backtrace.
func (g Goroutine) GomegaString() stringReturns Gomega-formatted representation without the backtrace.
var ReportFilenameWithPath = falseControls whether leak reports show abbreviated filenames (package name + filename) or full paths.
Default Behavior (false):
foo/bar.go:123With Full Paths (true):
/home/goworld/coolprojects/mymodule/foo/bar.go:123Usage Example:
import "github.com/onsi/gomega/gleak"
// Enable full paths in leak reports
gleak.ReportFilenameWithPath = true
// Now leak reports will show complete file paths
Eventually(gleak.Goroutines).ShouldNot(gleak.HaveLeaked())
// Restore default
gleak.ReportFilenameWithPath = falsepackage mypackage_test
import (
"testing"
"time"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/onsi/gomega/gleak"
)
func TestMyPackage(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "MyPackage Suite")
}
var _ = Describe("Service", func() {
var snapshot []gleak.Goroutine
BeforeEach(func() {
// Take snapshot before each test
snapshot = gleak.Goroutines()
})
AfterEach(func() {
// Verify no goroutines leaked during the test
Eventually(gleak.Goroutines).
WithTimeout(2 * time.Second).
WithPolling(250 * time.Millisecond).
ShouldNot(gleak.HaveLeaked(snapshot))
})
It("processes requests without leaking goroutines", func() {
service := NewService()
service.Start()
defer service.Stop()
// Test service operations
service.ProcessRequest("test")
// Leak detection happens automatically in AfterEach
})
Context("with background workers", func() {
It("ignores expected background goroutines", func() {
service := NewService()
service.Start()
defer service.Stop()
// Check for leaks, ignoring known background workers
Eventually(gleak.Goroutines).ShouldNot(
gleak.HaveLeaked(
snapshot,
gleak.IgnoringTopFunction("mypackage.(*Service).backgroundWorker"),
gleak.IgnoringCreator("mypackage.(*Service).Start"),
),
)
})
})
})HaveLeaked automatically includes built-in filters for common runtime and testing goroutines:
Ginkgo Framework:
IgnoringTopFunction("github.com/onsi/ginkgo/v2/internal.(*Suite).runNode")IgnoringTopFunction("github.com/onsi/ginkgo/v2/internal.(*Suite).runNode...")IgnoringTopFunction("github.com/onsi/ginkgo/v2/internal/interrupt_handler.(*InterruptHandler).registerForInterrupts...")Go Testing Package:
IgnoringTopFunction("testing.RunTests [chan receive]")IgnoringTopFunction("testing.(*T).Run [chan receive]")IgnoringTopFunction("testing.(*T).Parallel [chan receive]")Go Runtime:
IgnoringTopFunction("os/signal.signal_recv")IgnoringTopFunction("os/signal.loop")IgnoringInBacktrace("runtime.ensureSigM")IgnoringInBacktrace("runtime.ReadTrace")These filters ensure that legitimate framework and runtime goroutines are not reported as leaks.
import (
. "github.com/onsi/gomega"
"github.com/onsi/gomega/gleak"
)
// Combine multiple filter strategies
Eventually(gleak.Goroutines).ShouldNot(
gleak.HaveLeaked(
// Ignore pre-existing goroutines
snapshot,
// Ignore specific top functions
"myapp.backgroundMonitor",
"myapp.metricsCollector",
// Ignore by creator
gleak.IgnoringCreator("myapp.(*Server).Start"),
// Ignore anything with specific backtrace content
gleak.IgnoringInBacktrace("myapp/internal/cache"),
// Ignore with state specification
gleak.IgnoringTopFunction("myapp.worker [chan receive]"),
),
)Problem: False positives - legitimate goroutines reported as leaks
Solutions:
Eventually instead of Expect to allow time for cleanupHaveLeaked.WithTimeout(5 * time.Second)Problem: Unclear which goroutines are leaking
Solutions:
gleak.ReportFilenameWithPath = trueBacktrace field of leaked Goroutine objects.String() method on Goroutine for readable outputProblem: Need to debug filter matching
Solutions:
Expect for testingfmt.Printf("%+v\n", gleak.Goroutines())TopFunction, CreatorFunction, State