How to Spot and Prevent Common Go Memory Leaks

This article examines typical Go memory‑leak scenarios—including unclosed files, forgotten HTTP response bodies, slice sharing, goroutine and channel misuse, improper finalizers, and outdated ticker handling—provides concrete code examples, and offers practical techniques such as using strings.Clone, buffered channels, and proper defer usage to avoid leaks.

Radish, Keep Going!
Radish, Keep Going!
Radish, Keep Going!
How to Spot and Prevent Common Go Memory Leaks

Regardless of the programming language, memory leaks are a frequent problem, and writing leaking code in Go can be subtle. This article lists several scenarios that can cause memory leaks in Go and shows how to avoid them.

Resource Leaks

Not closing opened files

If you forget to call Close on a file, the process may exhaust its file descriptor limit and report the error too many open files. The following example demonstrates this:

func main() {
    files := make([]*os.File, 0)
    for i := 0; ; i++ {
        file, err := os.OpenFile("test.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
        if err != nil {
            fmt.Printf("Error at file %d: %v
", i, err)
            break
        } else {
            _, _ = file.Write([]byte("Hello, World!"))
            files = append(files, file)
        }
    }
}

On macOS a process can open up to 61 440 file handles (the limit can be changed manually). Go programs open three standard handles ( stderr, stdout, stdin), so the practical limit is 61 437.

http.Response.Body.Close()

Forgetting to close the body of an HTTP response also leaks memory. The code below omits the defer that closes the body:

func makeRequest() {
    client := &http.Client{}
    req, err := http.NewRequest(http.MethodGet, "http://localhost:8081", nil)
    res, err := client.Do(req)
    if err != nil {
        fmt.Println(err)
    }
    _, err = ioutil.ReadAll(res.Body)
    // defer res.Body.Close()
    if err != nil {
        fmt.Println(err)
    }
}

String / Slice Leaks

Although the Go spec does not guarantee that a substring shares the same backing array as the original string, the compiler often does, which can lead to temporary memory leaks. Using strings.Clone() forces a copy and avoids the leak:

func Demo1() {
    for i := 0; i < 10; i++ {
        s := createStringWithLengthOnHeap(1 << 20) // 1 MiB
        packageStr1 = append(packageStr1, strings.Clone(s[:50]))
    }
}

Goroutine Leak

Goroutine handler

Leaking goroutines quickly exhaust memory and cause OOM:

for {
    go func() {
        time.Sleep(1 * time.Hour)
    }()
}

Channel misuse

Improper use of unbuffered channels can block goroutines and cause leaks. The example below exits before the goroutine reads from the channel, leaving it hanging:

func Example() {
    a := 1
    c := make(chan error)
    go func() {
        c <- err
        return
    }()
    if a > 0 {
        return
    }
    err := <-c
}

Changing the channel to a buffered one resolves the issue:

c := make(chan error, 1)

Another typical mistake is using range on a channel without closing it, which blocks the ranging goroutine:

func main() {
    wg := &sync.WaitGroup{}
    c := make(chan any, 1)
    items := []int{1, 2, 3, 4, 5}
    for _, i := range items {
        wg.Add(1)
        go func() {
            c <- i
        }()
    }
    go func() {
        for data := range c {
            fmt.Println(data)
            wg.Done()
        }
        fmt.Println("close")
    }()
    wg.Wait()
    time.Sleep(1 * time.Second)
}

The correct approach is to close the channel after all sends are done, typically after wg.Wait().

runtime.SetFinalizer Misuse

If two objects both have runtime.SetFinalizer and reference each other, they form a reference cycle that the garbage collector cannot break, causing a leak even when the objects are no longer used.

time.Ticker

Before Go 1.23, failing to call ticker.Stop() could leak resources. Starting with Go 1.23 this issue is resolved.

defer

Using defer to release resources is fine, but two aspects can cause temporary leaks: long‑running functions where deferred calls are delayed, and placing defer inside loops, which allocates a defer record each iteration.

func ReadFile(files []string) {
    for _, file := range files {
        f, err := os.Open(file)
        if err != nil {
            fmt.Println(err)
            return
        }
        // do something
        defer f.Close()
    }
}

In such cases the deferred close may not happen promptly, leading to both temporary memory leaks and "too many open files" errors. Use explicit close calls when appropriate.

Summary

The article lists several patterns that can cause memory leaks in Go, with goroutine leaks being the most common. Misusing goroutines, channels (especially with select or range), and defer can make leaks hard to detect. Tools like pprof help locate leaks, enabling developers to write more robust code.

References

https://go101.org/article/memory-leaking.html

resource managementMemory LeakGoroutine
Radish, Keep Going!
Written by

Radish, Keep Going!

Personal sharing

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.