Underlying Principles and Operation Pitfalls of Slices in Go

Underlying Principles and Operation Pitfalls of Slices in Go

1. What is a Slice?

A slice (Slice) is a dynamic array structure in Go, consisting of three core fields:

  • Pointer: Points to the starting element of the underlying array (the position in the array corresponding to the first element of the slice).
  • Length (len): The number of elements currently contained in the slice.
  • Capacity (cap): The number of elements from the slice's starting position to the end of the underlying array.

Example:

arr := [5]int{1, 2, 3, 4, 5}
s := arr[1:3] // Slice 's' points to arr[1] to arr[2], len=2, cap=4 (from arr[1] to arr[4])

2. Ways to Create a Slice

(1) Direct Declaration

var s1 []int          // At this point, s1 is nil, len=0, cap=0
s2 := []int{1, 2, 3}  // Underlying array is [1,2,3], len=cap=3

(2) Creation via make

s := make([]int, 3, 5) // len=3, cap=5, underlying array initialized with zero values

(3) Slicing from an Array or Another Slice

arr := [3]int{1, 2, 3}
s := arr[0:2]         // Shares memory with the original array

3. Shared Underlying Arrays and Memory Pitfalls

Key Rules:

  • Multiple slices may share the same underlying array; modifying elements affects each other.
  • Expansion Mechanism: When appending elements exceeds capacity, a new array is allocated (capacity typically grows by a factor of 1.5 or 2).

Example 1: Shared Modification

s1 := []int{1, 2, 3}
s2 := s1[:2]          // s2=[1,2], cap=3
s2[0] = 9             // Modify the underlying array
fmt.Println(s1)       // [9,2,3]! s1 is affected

Example 2: Separation After Expansion

s1 := []int{1, 2, 3}
s2 := append(s1, 4)   // s1 capacity is 3, insufficient after appending, s2 points to a new array
s2[0] = 9
fmt.Println(s1)       // [1,2,3] (unchanged)

4. Analysis of Common Operation Pitfalls

(1) Slicing Operation Causing Capacity Leak

func main() {
    s1 := make([]int, 0, 10)
    s2 := s1[:5]       // s2's cap=10, but only 5 are actually used
    // At this point, the entire underlying array (10 elements) cannot be garbage collected, even though s2 only uses 5!
}

Solution: Explicitly specify capacity.

s2 := s1[:5:5]        // The third parameter limits cap=5, preventing memory leak

(2) Misunderstanding of Function Parameter Passing

func appendSlice(s []int) {
    s = append(s, 100) // If expansion occurs, 's' points to a new array, but the external slice still points to the original array
}

func main() {
    s := make([]int, 2, 3)
    appendSlice(s)
    fmt.Println(s)     // [0,0] (unchanged!)
}

Principle: A slice is passed by value (as a struct containing pointer, len, and cap); modifications to the pointer inside the function are not visible externally.
Correct Approach: Return the modified slice or use a pointer.

func appendSlice(s *[]int) {
    *s = append(*s, 100)
}

5. Slices and Performance Optimization

(1) Pre-allocate Capacity

Avoid frequent expansions:

// Inefficient
var s []int
for i := 0; i < 1000; i++ {
    s = append(s, i)   // May trigger multiple expansions
}

// Efficient
s := make([]int, 0, 1000)
for i := 0; i < 1000; i++ {
    s = append(s, i)
}

(2) Reusing Large Slices

Use copy instead of re-slicing:

// Incorrect: s2 still references the large array
s1 := make([]int, 10000)
s2 := s1[:10]         // The underlying array (10000 elements) cannot be freed

// Correct: Copy data to a new slice
s2 := make([]int, 10)
copy(s2, s1)

6. Summary of Key Points

  • Slices are reference types but are essentially passed by value as a struct.
  • Understand the underlying array sharing mechanism to avoid unintended modifications.
  • Pay attention to the difference between capacity and length; be wary of memory leaks when slicing.
  • Expansion changes the underlying array; return a new slice or use pointers when necessary.