3

This seems very strange, With in a loop there is a local variable slice with new value assigned for each loop and I'm appending that slice to a global sliceWrappers. After the loop completes, all values inside the global slice contains only reference to the last value set on that local slice variable.

Code:

package main

import (
    "fmt"
    "strconv"
)

func main() {
    var sliceWrappers [][]string
    initialSlice := append([]string{}, "hi")
    initialSlice = append(initialSlice, "there")
    
    // Remove this line and it works fine
    initialSlice = append(initialSlice, "I'm")
    
    for i := 0; i < 2; i++ {
        slice := append(initialSlice, strconv.Itoa(i))
        fmt.Printf("Slice Value : %+v, Initial Value : %+v\n", slice, initialSlice)
        sliceWrappers = append(sliceWrappers, slice)
    }

    for _, sliceWrapper := range sliceWrappers {
        fmt.Printf("%+v\n", sliceWrapper)
    }
}

Actual Output:

Slice Value : [hi there I'm 0], Initial Value : [hi there I'm]
Slice Value : [hi there I'm 1], Initial Value : [hi there I'm]
[hi there I'm 1]
[hi there I'm 1]

Expected Output:

Slice Value : [hi there I'm 0], Initial Value : [hi there I'm]
Slice Value : [hi there I'm 1], Initial Value : [hi there I'm]
[hi there I'm 0]  <------ This is not happening
[hi there I'm 1]

If I Remove initialSlice = append(initialSlice, "I'm") line, then it works perfectly.

Slice Value : [hi there 0], Initial Value : [hi there]
Slice Value : [hi there 1], Initial Value : [hi there]
[hi there 0]  <------ Works Perfectly
[hi there 1]

I Believe this has something to do with the append

The append built-in function appends elements to the end of a slice. If it has sufficient capacity, the destination is resliced to accommodate the new elements.

If the above condition was responsible for it, then shouldn't value of initialSlice which was printed inside the loop also be same as slice?

Playground - https://play.golang.org/p/b3SDGoA2Lzv

PS : I unfortunately wrote test cases for my code with just 3 levels nesting and it passed fine. I now have to take care of copy for slices inside loops.

Kishore Bandi
  • 5,537
  • 2
  • 31
  • 52
  • Possible duplicate: https://stackoverflow.com/q/15945030/13860 – Jonathan Hall Jul 14 '20 at 10:18
  • @Flimzy No, its different. In that question, its about modifying the original slice while the range is taking place. Here its a totally different loop, I'm just trying append a value to a slice & print it. It works if the original slice has 2 values, but fails if it has 3 values within it. – Kishore Bandi Jul 14 '20 at 10:24

2 Answers2

2

Slices are based around a pointer to an array, a length, and a capacity. You put slice in sliceWrappers on the first iteration (so it contains a pointer to an array). On the second iteration, the append(initialSlice, strconv.Itoa(i)) call changes the values in this same array, because the memory location hasn't changed. This array is pointed to by both slice in the first and second iteration, so both slices that end up in sliceWrappers point to the same data.

You can avoid this by copying the data to a new slice before adding it to sliceWrappers:

    for i := 0; i < 2; i++ {
        slice := append(initialSlice, strconv.Itoa(i))
        fmt.Printf("Slice Value : %+v, Initial Value : %+v\n", slice, initialSlice)
        copiedSlice := make([]string, len(slice))
        copy(copiedSlice, slice)
        sliceWrappers = append(sliceWrappers, copiedSlice)
    }

Which gives the expected output:

Slice Value : [hi there I'm 0], Initial Value : [hi there I'm]
Slice Value : [hi there I'm 1], Initial Value : [hi there I'm]
[hi there I'm 0]
[hi there I'm 1]

As for removing the line initialSlice = append(initialSlice, "I'm"): When you append to a slice, it will check whether it can fit the new length inside the capacity. If not, it will allocate a new array (and thereby new memory location). The shorter slice containing "hi there" is at its capacity and appending to it will allocate a new array and create a slice with a larger capacity.

  • If you have the line initialSlice = append(initialSlice, "I'm") in your program, the new array will be allocated before the loop. The append(...)s inside the loop won't cause a new allocation.
  • If you don't have the line in your program, the append(...)s inside the loop will cause a new allocation, so each ends up with a different memory location, which is why they don't overwrite each other.

My source is Go Slices: usage and internals https://blog.golang.org/slices-intro#TOC_4.

Nyubis
  • 528
  • 5
  • 12
  • I don't think the output is doing what you're saying. If that's the case then why is the code working if you remove this line `initialSlice = append(initialSlice, "I'm")` A pointer won't behave based on how many elements are inside the slice. – Kishore Bandi Jul 14 '20 at 12:26
  • Because then the length of the string that you try to put in there exceeds the capacity of the slice. `append` will then return a slice that has a pointer to a different location in memory. I probably phrased things a bit too simply in my first sentence, I'll edit it to clarify. – Nyubis Jul 14 '20 at 14:12
  • 1
    True, its similar to what I was thinking and what @nail explained. But I'm still not sure what's happening underneath. When it uses the same array (since it has capacity left), in that case the pointer address for the returned slice & original slice will be the same. However the values it shows is different, even thought they've same address. Any idea? https://play.golang.org/p/VbA2AIPAnFi Oh, got it. The slice internals, contain a pointer, length as fields. so while displaying it uses the length field to determine how many values to print even though they've the same pointer reference. – Kishore Bandi Jul 14 '20 at 15:19
2
// Remove this line and it works fine
//initialSlice = append(initialSlice, "I'm")
fmt.Printf("Slice Value : %p - %d\n", initialSlice, cap(initialSlice))
...
for i := 0; i < 2; i++ {
    slice := append(initialSlice, strconv.Itoa(i))
    fmt.Printf("Slice Value : %p - %p\n", slice, initialSlice)
    ...
}

print the address and capacity of initialSlice and slice as above. when uncomment the append I'm line. It output as below:

Slice Value : 0xc00009c000 - 2
Slice Value : 0xc0000a6000 - 0xc00009c000
Slice Value : 0xc00009e040 - 0xc00009c000

and if comment the line, output below:

Slice Value : 0xc00009e000 - 4
Slice Value : 0xc00009e000 - 0xc00009e000
Slice Value : 0xc00009e000 - 0xc00009e000

And then why it output as you expect when comment the line?
because in this scene, the capacity of initialSlice is 2. A new underlying array will be allocated when append new elements for it dont have enough space to achieve append action.
And when you uncomment the line, the capacity of initialSlice is 4, it will modify the array in place.

reference: append doc

The append built-in function appends elements to the end of a slice. If it has sufficient capacity, the destination is resliced to accommodate the new elements. If it does not, a new underlying array will be allocated. Append returns the updated slice. It is therefore necessary to store the result of append, often in the variable holding the slice itself: slice = append(slice, elem1, elem2) slice = append(slice, anotherSlice...) As a special case, it is legal to append a string to a byte slice, like this: slice = append([]byte("hello "), "world"...)

nail fei
  • 2,179
  • 3
  • 16
  • 36
  • 1
    +1 Perfect. That's exactly what I was suspecting. But with that explanation I had more doubts. If the same slice was reused (In Place modification) then why did the value of `initialSlice` always print the original value? I mean shouldn't the value of `slice` and `InitialSlice` be the same when printed? – Kishore Bandi Jul 14 '20 at 12:29
  • Basically this code should have printed the same values for both variables, as their addresses are the same. https://play.golang.org/p/VbA2AIPAnFi – Kishore Bandi Jul 14 '20 at 12:37