Background:
Slices are one of the key dynamic structures in Go, introduced as an alternative to fixed-length arrays for improved convenience and memory efficiency. They provide flexible manipulation of subsets of arrays but come with several nuances important for performant and safe code.
Problem:
Many developers do not understand how slices work: a slice is not the array itself, but a structure that contains a pointer to the array, its length, and capacity. This can lead to memory leaks, bugs when working with copies, and unexpected effects when modifying the underlying array.
Solution:
A slice is a type:
type slice struct { ptr unsafe.Pointer len int cap int }
When a slice is expanded using append(), it may trigger a reallocation of the backing array, and all previous references to the old array will remain valid but will point to stale data. Ignoring this feature can lead to errors and memory leaks.
Example of correct memory allocation and copying:
src := []int{1,2,3,4,5} dst := make([]int, len(src)) copy(dst, src)
A slice created using [:] shares the underlying array, and modifications will affect each other unless a copy is performed.
Key features:
What happens when increasing a slice using append exceeding its capacity if other slices have references to the same array?
Appending beyond capacity creates a new underlying array at a new memory location, and only this slice will reference the new array, while the others still reference the old one. This is a common cause of data discrepancies.
Why is it important not to hold long-lived slices of small size obtained from a large array?
Even if a slice is very small, its pointer holds a reference to the entire backing array, which could result in keeping the large array in memory and causing a memory leak.
What happens if you slice an array beyond its limits?
A panic will occur: runtime error: slice bounds out of range.
A function reads a large file into a byte array and returns a slice of the first 100 elements. This slice is then held for a long time, but the memory for the large array remains in GC.
Pros:
Cons:
Immediately after obtaining the slice, the required portion is copied into a new slice using make and copy. The old array is discarded, allowing GC to free the memory.
Pros:
Cons: