Reallocating underlying array of slice


When appending data into slice, if the underlying array of the slice doesn't have enough space, a new array will be allocated. Then the elements in old array will be copied into this new memory, accompanied with adding new data behind. So when using Go built-in append function, you must always keep the idea that "the array may have been changed" in mind, and be very careful about it, otherwise, it may bite you!

Let me explain it through a contrived example:

package main

import (
    "fmt"
)

func addTail(s []int)  {
    var ns [][]int
    for _, v := range []int{1, 2} {
        ns = append(ns, append(s, v))
    }
    fmt.Println(ns)
}

func main() {
    s1 := []int{0, 0}
    s2 := append(s1, 0)

    for _, v := range [][]int{s1, s2} {
        addTail(v)
    }
}   

The s1 is [0, 0], and the s2 is [0, 0, 0]; in addTail function, I want to add 1 and 2 behind the slice. So the wanted output is like this:

[[0 0 1] [0 0 2]]
[[0 0 0 1] [0 0 0 2]]

But the actual result is:

[[0 0 1] [0 0 2]]
[[0 0 0 2] [0 0 0 2]]

The operations on s1 are successful, while s2 not.

Let's use delve to debug this issue and check the internal mechanism of slice: Add breakpoint on addTail function, and it is first hit when processing s1:

(dlv) n
> main.addTail() ./slice.go:8 (PC: 0x401022)
     3: import (
     4:         "fmt"
     5: )
     6:
     7: func addTail(s []int)  {
=>   8:         var ns [][]int
     9:         for _, v := range []int{1, 2} {
    10:                 ns = append(ns, append(s, v))
    11:         }
    12:         fmt.Println(ns)
    13: }
(dlv) p s
[]int len: 2, cap: 2, [0,0]
(dlv) p &s[0]
(*int)(0xc82000a2a0)

The length and capacity of s1 are both 2, and the underlying array address is 0xc82000a2a0, so what happened when executing the following statement:

ns = append(ns, append(s, v))

Since the length and capacity of s1 are both 2, there is no room for new buddy. To append a new value, a new array must be allocated, and it contains both [0, 0] from s1 and the new value(1 or 2). You can consider append(s, v) generated an anonymous new slice, and it is appended in ns. We can check it after running "ns = append(ns, append(s, v))":

(dlv) n
> main.addTail() ./slice.go:9 (PC: 0x401217)
     4:         "fmt"
     5: )
     6:
     7: func addTail(s []int)  {
     8:         var ns [][]int
=>   9:         for _, v := range []int{1, 2} {
    10:                 ns = append(ns, append(s, v))
    11:         }
    12:         fmt.Println(ns)
    13: }
    14:
(dlv) p ns
[][]int len: 1, cap: 1, [
        [0,0,1],
]
(dlv) p ns[0]
[]int len: 3, cap: 4, [0,0,1]
(dlv) p &ns[0][0]
(*int)(0xc82000e240)
(dlv) p s
[]int len: 2, cap: 2, [0,0]
(dlv) p &s[0]
(*int)(0xc82000a2a0)

We can see the length of anonymous slice is 3, capacity is 4, and the underlying array address is 0xc82000e240, different from s1's (0xc82000a2a0). Continue executing until exit loop:

(dlv) n
> main.addTail() ./slice.go:12 (PC: 0x40124c)
     7: func addTail(s []int)  {
     8:         var ns [][]int
     9:         for _, v := range []int{1, 2} {
    10:                 ns = append(ns, append(s, v))
    11:         }
=>  12:         fmt.Println(ns)
    13: }
    14:
    15: func main() {
    16:         s1 := []int{0, 0}
    17:         s2 := append(s1, 0)
(dlv) p ns
[][]int len: 2, cap: 2, [
        [0,0,1],
        [0,0,2],
]
(dlv) p &ns[0][0]
(*int)(0xc82000e240)
(dlv) p &ns[1][0]
(*int)(0xc82000e280)
(dlv) p &s[0]
(*int)(0xc82000a2a0)

We can see s1, ns[0] and ns[1] have 3 independent array.

Now, let's follow the same steps to check what happened on s2:

(dlv) n
> main.addTail() ./slice.go:8 (PC: 0x401022)
     3: import (
     4:         "fmt"
     5: )
     6:
     7: func addTail(s []int)  {
=>   8:         var ns [][]int
     9:         for _, v := range []int{1, 2} {
    10:                 ns = append(ns, append(s, v))
    11:         }
    12:         fmt.Println(ns)
    13: }
(dlv) p s
[]int len: 3, cap: 4, [0,0,0]
(dlv) p &s[0]
(*int)(0xc82000e220)

The length of s2 is 3, and capacity is 4, so there is one slot for adding new element. Check the s2 and ns' values after executing "ns = append(ns, append(s, v))" the first time:

(dlv)
> main.addTail() ./slice.go:9 (PC: 0x401217)
     4:         "fmt"
     5: )
     6:
     7: func addTail(s []int)  {
     8:         var ns [][]int
=>   9:         for _, v := range []int{1, 2} {
    10:                 ns = append(ns, append(s, v))
    11:         }
    12:         fmt.Println(ns)
    13: }
    14:
(dlv) p ns
[][]int len: 1, cap: 1, [
        [0,0,0,1],
]
(dlv) p &ns[0][0]
(*int)(0xc82000e220)
(dlv) p s
[]int len: 3, cap: 4, [0,0,0]
(dlv) p &s[0]
(*int)(0xc82000e220)

We can see the new anonymous slice's array address is also 0xc82000e220, that's because the s2 has enough space to hold new value, no new array is allocated. Check the s2 and ns again after adding 2:

(dlv)
> main.addTail() ./slice.go:12 (PC: 0x40124c)
     7: func addTail(s []int)  {
     8:         var ns [][]int
     9:         for _, v := range []int{1, 2} {
    10:                 ns = append(ns, append(s, v))
    11:         }
=>  12:         fmt.Println(ns)
    13: }
    14:
    15: func main() {
    16:         s1 := []int{0, 0}
    17:         s2 := append(s1, 0)
(dlv) p ns
[][]int len: 2, cap: 2, [
        [0,0,0,2],
        [0,0,0,2],
]
(dlv) p &ns[0][0]
(*int)(0xc82000e220)
(dlv) p &ns[1][0]
(*int)(0xc82000e220)
(dlv) p s
[]int len: 3, cap: 4, [0,0,0]
(dlv) p &s[0]
(*int)(0xc82000e220)

All 3 slices point to the same array, so the later value(2) will override previous item(1).

So in a conclusion, append is very tricky since it can modify the underlying array without noticing you. You must know the memory layout behind every slice clearly, else the slice can give you a big, unwanted surprise!

results matching ""

    No results matching ""