Go Slices: Common pitfalls, hidden behaviors, and how to avoid bugs
Go Slices: Common pitfalls, hidden behaviors, and how to avoid bugs
Introduction
Slices are one of Go’s most used data structures, and also one of the most misunderstood at an advanced level. They look simple on the surface, but subtle behaviors around memory sharing, capacity growth, and mutation can quietly introduce bugs in production systems.
This tutorial focuses on real caveats, common mistakes, and practical patterns around Go slices. The goal is not to explain what a slice is—that’s assumed—but to explain how slices behave under pressure: in data pipelines, backend services, and performance-sensitive systems.
Background
A Go slice is not an array. It is a descriptor that contains three things:
- a pointer to an underlying array
- a length
- a capacity
The key mental model is this:
Multiple slices can point to the same underlying array.
This single fact explains most slice-related bugs. Appending, slicing, or passing slices between functions can unexpectedly mutate data elsewhere if you are not careful about ownership and capacity.
Scenario
Imagine a financial risk engine that processes time-ordered price ticks. Each stage of the pipeline receives a slice of prices, applies transformations, and forwards the result.
One stage trims outliers. Another aggregates values. A third persists results.
If one stage accidentally mutates data owned by another stage, you don’t get a crash. You get silent financial miscalculations.
That’s far worse.
The Problem
Create a new file:
touch slice_bug.go
Run it with:
go run slice_bug.go
package main
import "fmt"
func removeOutliers(prices []float64) []float64 {
// Remove first and last value (simple outlier trimming)
return prices[1 : len(prices)-1]
}
func main() {
rawPrices := []float64{100.0, 101.5, 102.0, 500.0, 103.0}
trimmed := removeOutliers(rawPrices)
trimmed[0] = 999.0
fmt.Println("Raw prices:", rawPrices)
fmt.Println("Trimmed prices:", trimmed)
}
Code Execution
When the program runs, the output is surprising:
Raw prices: [100 999 102 500 103]
Trimmed prices: [999 102 500]
The removeOutliers function appears to return a new slice.
In reality, it returns a view into the same underlying array.
Mutating trimmed mutated rawPrices.
In a real system, this kind of bug can propagate incorrect values across unrelated pipeline stages.
Understanding Slice Aliasing
The bug does not come from using slice expressions, it comes from assuming that slicing creates independent data.
prices[1:len(prices)-1] creates a new slice header, but it still points to the same underlying array as the original slice.
That means:
- Both slices see the same memory
- Mutating elements through one slice mutates the other
- This happens silently, with no compiler warning
So the real issue is aliasing: multiple slices referencing the same backing array without clear ownership rules.
Explicit Copy to Break Memory Sharing
package main
import "fmt"
func removeOutliers(prices []float64) []float64 {
result := make([]float64, len(prices)-2)
copy(result, prices[1:len(prices)-1])
return result
}
func main() {
rawPrices := []float64{100.0, 101.5, 102.0, 500.0, 103.0}
trimmed := removeOutliers(rawPrices)
trimmed[0] = 999.0
fmt.Println("Raw prices:", rawPrices)
fmt.Println("Trimmed prices:", trimmed)
}
makeallocates a new backing arraycopycopies only the needed values- The returned slice owns its memory
Why this works well
Ownership is explicit. Each pipeline stage can safely mutate its data.
What about alternatives?
- Returning subslices assumes callers understand memory aliasing
- Documenting “do not mutate” is not enforcement
- Defensive copying at call sites scatters responsibility
Capacity Traps When Appending
A more subtle bug appears when appending.
package main
import "fmt"
func enrichPrices(prices []float64) []float64 {
return append(prices, 104.0)
}
func main() {
base := []float64{100, 101, 102}
view := base[:2]
enriched := enrichPrices(view)
fmt.Println("Base:", base)
fmt.Println("View:", view)
fmt.Println("Enriched:", enriched)
}
Depending on capacity, append may:
- allocate a new array (safe)
- reuse the existing array (dangerous)
This makes behavior data-dependent, which is unacceptable in critical systems.
Why this is risky
The caller cannot predict whether append mutates shared memory.
Capacity Limiting to Enforce Safety
package main
import "fmt"
func enrichPrices(prices []float64) []float64 {
safe := prices[:len(prices):len(prices)]
return append(safe, 104.0)
}
func main() {
base := []float64{100, 101, 102}
view := base[:2]
enriched := enrichPrices(view)
fmt.Println("Base:", base)
fmt.Println("View:", view)
fmt.Println("Enriched:", enriched)
}
- The full slice expression sets
capacity = length appendis forced to allocate new memory
Why this is better
It guarantees isolation without copying upfront.
What about other approaches?
- Blind copying wastes memory when append is rare
- Relying on current capacity is brittle and unsafe
Execution remains the same location and command.
Avoiding Hidden Retention (Memory Leaks)
Slices can accidentally keep large arrays alive.
package main
import "fmt"
func extractHeader(buffer []byte) []byte {
return buffer[:16]
}
func main() {
hugeBuffer := make([]byte, 10_000_000)
header := extractHeader(hugeBuffer)
fmt.Println(len(header))
}
Problem
header keeps a reference to the entire 10MB buffer.
Correct approach
package main
import "fmt"
func extractHeader(buffer []byte) []byte {
header := make([]byte, 16)
copy(header, buffer[:16])
return header
}
func main() {
hugeBuffer := make([]byte, 10_000_000)
header := extractHeader(hugeBuffer)
fmt.Println(len(header))
}
Why this is important
In servers, this mistake leads to unexplained memory growth under load.
Summary
Slices are powerful but dangerous when their memory behavior is misunderstood.
Key takeaways:
- Slicing does not copy data
- Appending can mutate shared memory
- Capacity controls safety
- Small subslices can retain huge buffers
- Ownership must be explicit in real systems
Understanding these rules is essential for writing correct, predictable Go code at scale.
English
Română