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.
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
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
