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.