Files

4.0 KiB

Resource Management Patterns

Defer Close Immediately

defer Close() MUST be called immediately after opening — NEVER delay. This prevents leaks when code is modified later and new return paths are added:

// Good — defer is right next to open
f, err := os.Open(path)
if err != nil {
    return err
}
defer f.Close()

// Bad — Close() is far from Open(), easy to forget when adding early returns
f, err := os.Open(path)
if err != nil {
    return err
}
// ... 50 lines of code ...
f.Close() // might never run if a new return is added above

This applies to all closeable resources: files, SQL rows, HTTP response bodies, gzip readers, bufio scanners wrapping readers, etc.

resp, err := http.Get(url)
if err != nil {
    return err
}
defer resp.Body.Close()

rows, err := db.QueryContext(ctx, query)
if err != nil {
    return err
}
defer rows.Close()

runtime.AddCleanup over runtime.SetFinalizer

runtime.AddCleanup SHOULD be preferred over runtime.SetFinalizer (Go 1.24+):

type Resource struct {
    handle uintptr
}

func NewResource() *Resource {
    r := &Resource{handle: acquireHandle()}
    runtime.AddCleanup(r, func(handle uintptr) {
        releaseHandle(handle)
    }, r.handle)
    return r
}

AddCleanup is preferred because:

  • Multiple cleanups can be attached to the same object
  • The cleanup function receives a copy of the value, not the object itself — no resurrection risk
  • Cleanups run even if the object is part of a cycle

Resource Pools

Resource pools SHOULD use channels with a fixed capacity for bounded allocation. Use channel-based pools or sync.Pool to manage limited resources between consumers. Always set a maximum size:

type ConnPool struct {
    conns chan *Conn
}

func NewConnPool(maxSize int, factory func() (*Conn, error)) (*ConnPool, error) {
    pool := &ConnPool{
        conns: make(chan *Conn, maxSize),
    }
    // Pre-fill with initial connections
    for range maxSize {
        conn, err := factory()
        if err != nil {
            return nil, fmt.Errorf("creating connection: %w", err)
        }
        pool.conns <- conn
    }
    return pool, nil
}

func (p *ConnPool) Get(ctx context.Context) (*Conn, error) {
    select {
    case conn := <-p.conns:
        return conn, nil
    case <-ctx.Done():
        return nil, ctx.Err()
    }
}

func (p *ConnPool) Put(conn *Conn) {
    select {
    case p.conns <- conn:
    default:
        conn.Close() // pool is full, discard
    }
}

Graceful Shutdown

Graceful shutdown MUST use signal.NotifyContext for clean termination. All resources (connections, files, channels) MUST be drained before process exit. Use os/signal and context cancellation:

func main() {
    ctx, stop := signal.NotifyContext(context.Background(),
        syscall.SIGINT, syscall.SIGTERM,
    )
    defer stop()

    srv := &http.Server{Addr: ":8080", Handler: router}

    // Start server in background
    go func() {
        if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            slog.Error("server error", "error", err)
        }
    }()

    slog.Info("server started", "addr", ":8080")

    // Wait for interrupt signal
    <-ctx.Done()
    slog.Info("shutting down...")

    // Give outstanding requests time to complete
    shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()

    if err := srv.Shutdown(shutdownCtx); err != nil {
        slog.Error("shutdown error", "error", err)
    }

    // Close other resources: database connections, message queues, etc.
    db.Close()
    slog.Info("shutdown complete")
}

This pattern applies to any long-running service — gRPC servers, message consumers, background workers. The key elements are:

  1. Capture OS signals with signal.NotifyContext
  2. Start the server in a goroutine
  3. Block on context cancellation
  4. Shut down with a timeout to drain in-flight requests
  5. Close all remaining resources in order

For goroutine shutdown patterns, see the samber/cc-skills-golang@golang-concurrency skill.