Introduction

In Go, defer statements are not executed at the end of the loop iteration -- they are executed when the surrounding function returns. In a tight loop that opens files, creates connections, or allocates resources, this causes all deferred cleanups to pile up until the function exits, potentially exhausting file descriptors or memory.

This is a subtle Go gotcha that developers from other languages (where cleanup runs at scope exit) commonly encounter.

Symptoms

  • Application runs out of file descriptors after processing many files in a loop
  • Memory usage grows linearly with loop iterations despite defer cleanup
  • Error: "too many open files" after processing a few hundred items

Common Causes

  • defer inside a loop doesn't execute until the function returns, not at loop iteration end
  • Each iteration accumulates deferred calls that are all held until function exit
  • Loop processes thousands of items, each deferring a file close or resource release

Step-by-Step Fix

  1. 1.Wrap loop body in an anonymous function: Defer runs when the anonymous function returns.
  2. 2.```go
  3. 3.// BAD: defer accumulates until processAll returns
  4. 4.func processAll(files []string) error {
  5. 5.for _, f := range files {
  6. 6.fh, err := os.Open(f)
  7. 7.if err != nil {
  8. 8.return err
  9. 9.}
  10. 10.defer fh.Close() // NOT executed until processAll returns!
  11. 11.// process fh...
  12. 12.}
  13. 13.return nil
  14. 14.}

// GOOD: defer runs each iteration func processAll(files []string) error { for _, f := range files { if err := func() error { fh, err := os.Open(f) if err != nil { return err } defer fh.Close() // Runs at end of anonymous function return processFile(fh) }(); err != nil { return err } } return nil } ```

  1. 1.Use explicit Close instead of defer in loops: Call Close directly at the end of each iteration.
  2. 2.```go
  3. 3.func processAll(files []string) error {
  4. 4.for _, f := range files {
  5. 5.fh, err := os.Open(f)
  6. 6.if err != nil {
  7. 7.return fmt.Errorf("open %s: %w", f, err)
  8. 8.}

err = processFile(fh) fh.Close() // Explicit close, runs immediately if err != nil { return fmt.Errorf("process %s: %w", f, err) } } return nil } ```

  1. 1.Use runtime.SetFinalizer as safety net: Not a replacement for proper cleanup, but catches leaks.
  2. 2.```go
  3. 3.import "runtime"

type Resource struct { file *os.File }

func NewResource(path string) (*Resource, error) { fh, err := os.Open(path) if err != nil { return nil, err } r := &Resource{file: fh} runtime.SetFinalizer(r, func(r *Resource) { if r.file != nil { log.Printf("WARNING: Resource not properly closed: %s", path) r.file.Close() } }) return r, nil }

func (r *Resource) Close() error { runtime.SetFinalizer(r, nil) return r.file.Close() } ```

Prevention

  • Never use defer in a tight loop -- use explicit cleanup or wrap in a function
  • Use golangci-lint with the fatcontext and bodyclose linters
  • Set ulimit -n lower in development to catch file descriptor leaks early
  • Review all loops that open resources as part of code review checklist