20. Sub-slices and shared memory
Slicing a slice is free — you get a new header pointing into the same backing array. That's what makes it fast, and it's also the source of Go's most notorious bug class. This lesson is about seeing the sharing before it surprises you.
The slicing expression
package main
import "fmt"
func main() {
nums := []int{0, 1, 2, 3, 4, 5, 6}
fmt.Println(nums[2:5])
fmt.Println(nums[:3])
fmt.Println(nums[4:])
fmt.Println(nums[:])
}
s[low:high] includes low and excludes high — the same half-open range
Python uses. Omit either side for "from the start" / "to the end".
The result is a slice of length high-low. No elements were copied to
produce it.
Sub-slices share the array
Here's the part that bites:
package main
import "fmt"
func main() {
nums := []int{0, 1, 2, 3, 4, 5, 6}
window := nums[2:5]
fmt.Println("before:", nums, window)
window[0] = 999
fmt.Println("after: ", nums, window)
}
Writing through window changed nums. There is only one array; window
is a different view of it, not a different copy of the data.
This is deliberate, and mostly it's what you want — passing data[100:200]
to a function costs three words, not a hundred elements. But it means "I took
a slice of it" is not the same as "I made a copy of it".
Capacity extends to the end of the array
A sub-slice's capacity runs from its start to the end of the backing array, not to the end of the slice:
package main
import "fmt"
func main() {
nums := []int{0, 1, 2, 3, 4, 5, 6}
window := nums[2:5]
fmt.Println(window, "len:", len(window), "cap:", cap(window))
window = append(window, 42)
fmt.Println("window:", window)
fmt.Println("nums: ", nums)
}
window had spare capacity, so append didn't allocate a new array — it
wrote 42 into the slot that nums[5] was already using. Appending to a
sub-slice overwrote data in the parent.
That's the bug. It happens quietly, it only happens when there's spare capacity, and it can survive a lot of testing before it hurts you.
Three defences
1. Copy when you mean copy.
package main
import "fmt"
func main() {
nums := []int{0, 1, 2, 3, 4, 5, 6}
window := make([]int, 3)
copy(window, nums[2:5])
window[0] = 999
fmt.Println("nums: ", nums)
fmt.Println("window:", window)
}
2. Use a three-index slice to cap the capacity.
package main
import "fmt"
func main() {
nums := []int{0, 1, 2, 3, 4, 5, 6}
window := nums[2:5:5]
fmt.Println(window, "len:", len(window), "cap:", cap(window))
window = append(window, 42)
fmt.Println("window:", window)
fmt.Println("nums: ", nums)
}
s[low:high:max] sets the capacity to max-low. With cap == len, the very
next append is forced to allocate a fresh array, so the parent can't be
touched. This is what careful library code returns when it hands a caller a
sub-slice.
3. Don't hold a small slice of a huge array.
func firstLine(hugeFile []byte) []byte {
i := bytes.IndexByte(hugeFile, '\n')
return hugeFile[:i] // keeps the WHOLE file alive
}
The returned 40-byte slice points into a 200 MB array, and the garbage collector can't free any of it while that slice exists. If you're keeping a small piece of something large, copy the piece out.
Slices of slices are shallow too
The same logic applies one level up:
package main
import "fmt"
func main() {
grid := [][]int{
{1, 2, 3},
{4, 5, 6},
}
row := grid[0]
row[0] = 100
fmt.Println(grid)
for _, r := range grid {
for _, v := range r {
fmt.Print(v, " ")
}
fmt.Println()
}
}
grid[0] isn't a copy of the row — it's the row. Copying the outer slice
would still leave every inner slice shared; a deep copy means copying each
row.
Deleting an element
There's no remove built-in. The idiomatic delete uses append and a spread:
package main
import "fmt"
func main() {
items := []string{"a", "b", "c", "d", "e"}
i := 2
items = append(items[:i], items[i+1:]...)
fmt.Println(items)
}
Read it as "everything before index i, followed by everything after it".
Note that this does modify the original backing array — after this call the
old items you may have handed to somebody else is scrambled. That's
consistent with everything above: one array, many views.
Your turn
Given data, produce an independent copy of the middle three elements
(indexes 1, 2, 3), change its first element to 0, and print both slices so
the original is untouched:
[10 20 30 40 50]
[0 30 40]
package main
import "fmt"
func main() {
data := []int{10, 20, 30, 40, 50}
// make an independent copy of data[1:4] called mid, then set mid[0] = 0
fmt.Println(data)
fmt.Println(mid)
}
package main
import "fmt"
func main() {
data := []int{10, 20, 30, 40, 50}
mid := make([]int, 3)
copy(mid, data[1:4])
mid[0] = 0
fmt.Println(data)
fmt.Println(mid)
}
Next: Go's other workhorse container — maps.