Introduction
Slices seem simple but hide complexity that trips up even experienced Go developers. The shared backing array behavior can cause subtle bugs, and understanding when append reallocates is crucial for correctness. This lesson covers essential operations and the gotchas you must know.
Key Concepts
Append: Built-in function that adds elements, potentially reallocating if capacity is exceeded.
Copy: Built-in function that copies elements between slices, returning the count copied.
Shared Backing Array: Multiple slices can reference the same underlying array, meaning modifications affect all.
Full Slice Expression: Syntax s[low:high:max] that limits capacity, preventing unintended sharing.
Real World Context
The shared backing array gotcha causes real production bugs: caching a slice, modifying it later, and corrupting the cache. Understanding these patterns is essential for concurrent code where race conditions can arise from shared slice data.
Deep Dive
Append Operations
gos := []int{1, 2} s = append(s, 3) // Single element s = append(s, 4, 5, 6) // Multiple elements s = append(s, other...) // Another slice (spread) // IMPORTANT: Always reassign! s = append(s, x) // ✓ append(s, x) // ✗ Result is lost!
Copy Function
gosrc := []int{1, 2, 3} dst := make([]int, len(src)) n := copy(dst, src) // n = 3 // Copy only copies min(len(dst), len(src)) elements small := make([]int, 2) copy(small, src) // Only copies [1, 2]
The Shared Array Gotcha
goa := []int{1, 2, 3, 4, 5} b := a[1:3] // b = [2, 3], shares a's array b[0] = 99 fmt.Println(a) // [1 99 3 4 5] - a was modified!
Append Can Share OR Reallocate
goa := make([]int, 3, 5) // len=3, cap=5 b := append(a, 4) // Room in capacity: shares array! a[0] = 99 fmt.Println(b[0]) // 99 - b was affected! // But if capacity exceeded: c := append(b, 5, 6) // Needs reallocation b[0] = 0 fmt.Println(c[0]) // 99 - c has independent array
Safe Slice Copy
go// Create independent copy original := []int{1, 2, 3} safeCopy := make([]int, len(original)) copy(safeCopy, original) // Or using append safeCopy := append([]int(nil), original...)
Deleting Elements
go// Delete at index i s = append(s[:i], s[i+1:]...) // Using slices package (Go 1.21+) import "slices" s = slices.Delete(s, i, i+1)
Common Pitfalls
-
Forgetting to reassign append result:
append(s, x)withouts =silently discards the result. -
Assuming append always reallocates: When capacity suffices, append modifies the existing backing array, affecting other slices.
-
Modifying function parameters: Slices passed to functions share the backing array—modifications persist after return.
Best Practices
- Always assign
appendresult:s = append(s, x). - Use
copyorappend([]T(nil), s...)to create independent slices. - Use full slice expression
s[low:high:high]to limit capacity and prevent sharing. - Use
slices.Deleteinstead of manual append for clarity.
Summary
append adds elements and may reallocate if capacity is exceeded. copy transfers elements between slices. Multiple slices can share a backing array, causing modifications to propagate unexpectedly. Use the full slice expression or explicit copying to create independent slices.
Code Examples
package main
import "fmt"
func main() {
// Gotcha: shared backing array
a := []int{1, 2, 3, 4, 5}
b := a[1:3] // b = [2, 3]
b[0] = 99
fmt.Println(a) // [1 99 3 4 5] - a was modified!
fmt.Println(b) // [99 3]
}