Why do slices sometimes change when passed by value in Go?

Don’t know if you have found within a function of the parameters of the slices were sorted and will change function outside the original the order of the elements in a slice, but after the function section increased introversion elements outside the function of the original section but no new elements, more strange is added and sorted, external slice possible number of elements and element order will not change, is this why? We use three quizzes to explain why this happens.

Test a

What is the output of the following code?

func main(a) {
  var s []int
  for i := 1; i <= 3; i++ {
    s = append(s, i)
  }
  reverse(s)
  fmt.Println(s)
}

func reverse(s []int) {
  for i, j := 0.len(s) - 1; i < j; i++ {
    j = len(s) - (i + 1)
    s[i], s[j] = s[j], s[i]
  }
}
Copy the code

Run it on the Go Playground – play.golang.org/p/faJ3WNxpR…

In the above code, though s is passed by value, why is it still visible externally after a function call?

We all know that a slice is a pointer to the underlying array, and a slice itself does not store any data. This means that even though slices are passed by value here, slices in the function still point to the same memory address. So the slice used inside reverse() is a different pointer object, but will still point to the same memory address and share the same array. So after the function call, the numbers in the array are rearranged, and the slices outside the function share the same underlying array as the slices inside, so the outer s shows up as being sorted as well.

Test two

We’ll change the code slightly inside the reverse () function, adding a single append call to the function. How does it change our output?

func main(a) {
  var s []int
  for i := 1; i <= 3; i++ {
    s = append(s, i)
  }
  reverse(s)
  fmt.Println(s)
}

func reverse(s []int) {
  s = append(s, 999)
  for i, j := 0.len(s) - 1; i < j; i++ {
    j = len(s) - (i + 1)
    s[i], s[j] = s[j], s[i]
  }
}
Copy the code

Run it on the Go Playground – play.golang.org/p/tZpkaLA9c…

This time, when I print s out of the function you can see that it keeps the sorted order, but what happened to the previous element 1?

Let’s first look at the definition of slice

type slice struct {
  array unsafe.Pointer
  len   int
  cap   int
}
Copy the code

When we call Append, a new slice will be created. The new slice has a new “length” property, which is not a pointer but still points to the same array. Therefore, the code in our function will eventually reverse the array referenced by the original slice, but the length property of the original slice is the same as the previous length value, which is why the 1 above is discarded.

The final test

Finally we add some extra numbers to the slice inside the reverse () function. After the function is executed, print the external slice s to see what is output.

func main(a) {
  var s []int
  for i := 1; i <= 3; i++ {
    s = append(s, i)
  }
  reverse(s)
  fmt.Println(s)
}

func reverse(s []int) {
  s = append(s, 999.1000.1001)
  for i, j := 0.len(s)- 1; i < j; i++ {
    j = len(s) - (i + 1)
    s[i], s[j] = s[j], s[i]
  }
}
Copy the code

Run it on the Go Playground – play.golang.org/p/dnbKtLZG8…

In our final test, not only the slice length was not retained, but also the slice order was not affected. Why is that?

As mentioned earlier, when we call Append, a new slice is created. In the second test, this new slice still points to the same underlying array, because it has enough capacity to add new elements, so the array doesn’t change, but in this example, we added three elements, and our slice doesn’t have enough capacity. So the system allocates a new array and makes the slice point to that array. When we finally start reversing the elements in the slice within the Reverse function, it no longer affects our original array, but runs on a completely different array.

Verify our conclusion by cap function

We can verify what is happening by using the cap function to check the size of the slice passed to reverse ().

func reverse(s []int) {
  newElem := 999
  for len(s) < cap(s) {
    fmt.Println("Adding an element:", newElem, "cap:".cap(s), "len:".len(s))
    s = append(s, newElem)
    newElem++
  }
  for i, j := 0.len(s)- 1; i < j; i++ {
    j = len(s) - (i + 1)
    s[i], s[j] = s[j], s[i]
  }
}
Copy the code

Run it on the Go Playground – play.golang.org/p/SBHRj4dPF…

As long as we don’t exceed the size of the slice, we’ll eventually see changes made to the slice by the Reverse function in the main () function. We will still not see the length change, but we will see a rearrangement of the elements in the underlying array of slices.

If append () is called on S after the slice has been filled to capacity, we won’t see these changes in main () anymore because the code in our reverse function points a new slice to a completely different array.

Slices derived from slices or arrays are also affected

We can see the same effect if we happen to create a new slice in our code that is derived from an existing slice or array. For example, if you call s2: = s [:] and then pass S2 into our reverse () function, you might still end up affecting S because s2 and S both point to the same supported array. Similarly, if we append new elements to S2 and end up exceeding the supported array, we will no longer see changes to one slice affecting the other.

Strictly speaking, this is not a bad thing. Through before the absolute need not copied based array, we finally get to more efficient code, but need to consider when writing code to this point, so want to ensure that you can see outside a function change of section function program, then in the function must be the new section of the return to outside, even if the section is a reference type. That’s why you don’t want to bring other programming language experience to Go.

This problem is not limited to slice types

It’s not limited to slicing. Slicing is the most vulnerable type to fall into this trap, but any type with Pointers can be affected. As shown below.

type A struct {
  Ptr1 *B
  Ptr2 *B
  Val B
}

type B struct {
  Str string
}

func main(a) {
  a := A{
    Ptr1: &B{"ptr-str-1"},
    Ptr2: &B{"ptr-str-2"},
    Val: B{"val-str"},
  }
  fmt.Println(a.Ptr1)
  fmt.Println(a.Ptr2)
  fmt.Println(a.Val)
  demo(a)
  fmt.Println(a.Ptr1)
  fmt.Println(a.Ptr2)
  fmt.Println(a.Val)
}

func demo(a A) {
  // Update a value of a pointer and changes will persist
  a.Ptr1.Str = "new-ptr-str1"
  // Use an entirely new B object and changes won't persist
  a.Ptr2 = &B{"new-ptr-str-2"}
  a.Val.Str = "new-val-str"
}
Copy the code

Run it on the Go Playground → play.golang.org/p/8X-57DvgM…

Similar to this example, a slice in Go is defined as follows:

type slice struct {
  array unsafe.Pointer
  len   int
  cap   int
}
Copy the code

Notice that the array field is actually a pointer? This means that a slice will behave like any other type with nested Pointers in Go, which is not really special at all, it just happens to be the type that few people care about inside.

Ultimately, this means that developers need to know the types of data they are passing and how the functions they are calling might affect them. When you pass slices to other functions or methods, you should be aware that the function may or may not change elements in the original slice.

Also, you should always be aware that structures with Pointers inside can easily fall into the same situation. Unless the pointer itself is updated to refer to another object in memory, any changes to the data inside the pointer are preserved.