or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

core-assertions.mdgbytes.mdgcustom.mdgexec.mdghttp.mdgleak.mdgmeasure.mdgstruct.mdindex.mdmatchers.mdtypes.md
tile.json

gleak.mddocs/

gleak Package

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.

Overview

Goroutine leaks are a common problem in concurrent Go programs. The gleak package helps detect these leaks by:

  • Capturing snapshots of goroutines at test start
  • Comparing current goroutines against the baseline
  • Filtering out known system and background goroutines
  • Providing detailed information about leaked goroutines

Core Functions

IgnoreCurrent

{ .api }

func IgnoreCurrent() IgnoredGoroutines

Returns 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))

IgnoreBackgroundGoroutines

{ .api }

func IgnoreBackgroundGoroutines() IgnoredGoroutines

Returns 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:

  • Runtime finalizer goroutines
  • Testing framework goroutines
  • Signal handling goroutines
  • GC and system goroutines

Types

IgnoredGoroutines

{ .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.

Matchers

HaveLeaked

{ .api }

func HaveLeaked(options ...IgnoredGoroutines) types.GomegaMatcher

Creates 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 apply

Returns: 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(),
    ),
)

goroutine Sub-package

The gleak/goroutine sub-package provides utilities for inspecting goroutines.

Goroutine Type

{ .api }

type Goroutine struct {
    ID    uint64
    State string
    Stack string
    // Additional fields
}

Represents information about a single goroutine.

Fields:

  • ID - Unique goroutine ID
  • State - Current state (e.g., "running", "waiting", "syscall")
  • Stack - Stack trace of the goroutine

All Function

{ .api }

func All() []Goroutine

Returns 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)
}

Current Function

{ .api }

func Current() Goroutine

Returns information about the current goroutine.

Returns: A Goroutine object

Example:

current := goroutine.Current()
fmt.Printf("Current goroutine ID: %d\n", current.ID)

Complete Examples

Basic Leak Detection

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))
    })
})

Proper Cleanup Pattern

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))
    })
})

Testing Server Cleanup

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))
    })
})

Testing Channel Cleanup

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))
    })
})

Multiple Ignore Filters

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(),
            ),
        )
    })
})

Using Eventually for Graceful Shutdown

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))
    })
})

Inspecting Leaked Goroutines

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))
    })
})

Suite-Level Leak Detection

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
        })
    })
})

Testing Ticker Cleanup

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))
    })
})

Context Cancellation Pattern

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))
    })
})

Best Practices

1. Establish Baseline Early

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))
})

2. Use Eventually for Async Cleanup

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))

3. Always Clean Up Resources

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())

4. Use Context for Cancellation

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
    }
}()

5. Test Cleanup in AfterEach

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() { ... })
})

6. Combine with Cleanup Verifiers

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))
})

Common Leak Scenarios

Unbuffered Channel Deadlock

// 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
}()

Missing Context Cancellation

// 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()
        }
    }
}()

Forgotten Ticker Stop

// 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
        }
    }
}()

Server Not Shutdown

// 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)

Integration Patterns

With Ginkgo's AfterEach

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
})

With Custom Cleanup Timeout

AfterEach(func() {
    // Give more time for complex cleanup
    Eventually(gleak.Goroutines, "10s", "500ms").ShouldNot(
        gleak.HaveLeaked(baseline),
        "All goroutines should exit within 10 seconds",
    )
})