A memory leak in Go can mean memory remains in use because a small value still refers to a much larger allocation. It can also result from repeatedly creating resources that remain allocated longer than expected. Check what remains reachable and when a resource is released before assuming that a short slice, substring, or timeout is cheap. section 3.10.2 section 5.6 section 10.2
Three mistakes have different fixes:
- Reslicing a large slice to keep only a few elements can retain its backing array. If discarded elements contain pointers or pointer fields, their referenced data can remain reachable too. Copy the elements to keep into a new slice, or set the discarded references to nil. Copying iterates over kept elements; clearing iterates over discarded ones, so benchmark when that difference matters. section 3.10.2
- Storing `log[:36]` as a UUID can retain an entire, much larger log string. With the standard Go compiler, the substring shares its backing data. Store an independent copy, such as `strings.Clone(log[:36])`, when the original log need not remain in memory. section 5.6
- Calling `time.After(time.Hour)` on every iteration of a busy receive loop creates timeout resources that remain until they expire, even when a message wins the `select`. Use a reusable `time.NewTimer`, or create a timeout context per iteration and cancel it when a message arrives; the context approach also creates resources each iteration. section 10.2
Two elements, a thousand allocations
Suppose each of 1,000 `Foo` values holds a 1 MiB byte slice. Returning `foos[:2]` appears to keep only two values, but a forced garbage collection still leaves the allocation near 1 GiB. Copying the two values into a new slice, or clearing `foos[i].v` for every discarded element, breaks the unwanted retention. section 3.10.2
func main() {
foos := make([]Foo, 1_000) #1
printAlloc()
for i := 0; i < len(foos); i++ { #2
foos[i] = Foo{
v: make([]byte, 1024*1024),
}
}
printAlloc()
two := keepFirstTwoElementsOnly(foos) #3
runtime.GC() #4
printAlloc()
runtime.KeepAlive(two) #5
}
func keepFirstTwoElementsOnly(foos []Foo) []Foo {
return foos[:2]
}
The allocation experiment shows the reslice and the explicit garbage-collection check. section 3.10.2
func consumer(ch <-chan Event) {
timerDuration := 1 * time.Hour
timer := time.NewTimer(timerDuration) #1
for { #2
timer.Reset(timerDuration) #3
select {
case event := <-ch:
handle(event)
case <-timer.C: #4
log.Println("warning: no messages received")
}
}
}
A single timer replaces the timeout created on every receive-loop iteration. section 10.2
The timer example concerns resources retained until a timeout expires, not necessarily forever. Its allocation estimate—about 200 bytes per `time.After` call—is specifically for Go 1.15. Likewise, substring data sharing describes the standard Go compiler, not a guarantee imposed by the Go specification. section 10.2 section 5.6




