SoFunction
Updated on 2025-03-04

What have you stepped on in these pitfalls of Go Slice expansion?

Preface

I shared my experience of trampling on Go language for loops before"Go for range accidentally fell into the pit", everyone said it was useful.

Today, I also shared my experience of slicing Slice's append operation. I hope it will be helpful to my friends. Please support it three times if it is useful.

Revisiting knowledge

Definition of the underlying structure of slices: Contains pointers, lengths and capacity to the underlying array

type slice struct {
  array 
  len   int
  cap   int
}

append operation: It can be 1, multiple, or even the entire slice (remember to add... later); when the capacity is insufficient when adding elements, the slice expansion mechanism will be automatically triggered, resulting in a slice copy, and the pointer to the underlying array will change.

var nums []int
nums = append(nums, 1)
nums = append(nums, 2, 3, 4)
nums2 := []int{5, 6, 7}
nums = append(nums, nums2...)
(nums) //[1 2 3 4 5 6 7]

Case 1: Value passed + unexpanded

Let’s first see what results will be output below?

func main() {
  s1 := make([]int, 0, 5)
  ("s1 slice: ", s1)
  appendFunc(s1)
  ("s1 slice: ", s1)
  ("s1 slice expression: ", s1[:5])
}

func appendFunc(s2 []int) {
  s2 = append(s2, 1, 2, 3)
  ("s2 slice: ", s2)
}

Output result:

s1 slice:  []
s2 slice:  [1 2 3]
s1 slice:  []
s1 slice expression: [1 2 3 0 0]

When you see this result, you will have questions.It is obvious that the slice is a reference type, why after s2 appends a new element, s2 has a value but s1 is still empty, and can you get the value with a slice expression for s1?

Before the reason analysis, let’s first check whether s1 and s2 are the same slice, and print the address to verify it.

func main() {
  s1 := make([]int, 0, 5)
  ("s1 slice address: %p\n", s1)
  appendFunc(s1)
  //...
}

func appendFunc(s2 []int) {
  s2 = append(s2, 1, 2, 3)
  ("s2 slice address: %p\n", s2)
  //...
}

Output result:

S1 slice address: 0xc000018150
s2 slice address: 0xc000018150

It's time to see thisI was stunned. The addresses of the two slices are the same. After s2 is modified, s1 should also be modified simultaneously. There should be values.

We have to continue to delve into it.fmt package %pWhose address is printed?

//fmt/
func (p *pp) fmtPointer(value , verb rune) {
  var u uintptr
  switch () {
  case , , , , , :
    u = ()
  default:
    (verb)
    return
  }
  //...
}

//reflect/
func (v Value) Pointer() uintptr {
  k := ()
  switch k {
  //...
  
  case Slice:
    return (*SliceHeader)().Data
  }
  panic(&ValueError{"", ()})
}

By analyzing the source code of the fmt package, it is not difficult to find thatThe printed address is actually the address stored in the slice pointer to the underlying array, not the address of the two slices themselves.. It also means that these two slices point to the same underlying array.

The cause is formally analyzed

  • Value transfer operation, s1 and s2 are two different slice variables, but the pointer to the underlying array is the same;
  • Changes in length and capacity: s1 Len=0 and Cap=5, no changes occurred later; when s2 is assigned at the beginning, Len=0 and Cap=5, after the append operation, Len=3 and Cap=5, and the underlying array value is from[0,0,0,0,0]Modified to[1,2,3,0,0];
  • Output result, s1 outputs empty[] because Len=0, while s1 uses a slice expression, which is based on the underlying array[1,2,3,0,0]Slicing, so the output is[1,2,3,0,0]

Case 2: Transfer value + expansion

If the number of elements in case 1 exceeds the slice capacity, triggering automatic expansion. What will be the output result?

func main() {
  s1 := make([]int, 0, 5)
  ("s1 slice: ", s1)
  appendFunc(s1)
  ("s1 slice: ", s1)
  ("s1 slice expression: ", s1[:5])
}

func appendFunc(s2 []int) {
  s2 = append(s2, 1, 2, 3, 4, 5, 6)
  ("s2 slice: ", s2)
}

Output result:

s1 slice:  []
s2 slice:  [1 2 3 4 5 6]
s1 slice:  []
s1 slice expression: [0 0 0 0 0 0]

Cause analysis

  • After expansion occurs, the underlying array pointed to by s2 will produce a copy, resulting in s1 and s2 no longer pointing to the same underlying array;
  • Changes in length and capacity: After s2 append, Len=6, Cap=10 and the underlying array values ​​are[1,2,3,4,5,6,0,0,0,0]; The operation of s2 does not affect the data of s1 at all. s1 is still Len=0, Cap=5 and the underlying array value is[0,0,0,0,0]
  • Output result, s2 outputs because Len=6[1,2,3,4,5,6], s1 outputs empty[] because Len=0, while s1 uses a slice expression, which is based on the underlying array[0,0,0,0,0]Slicing, so the output is[0,0,0,0,0]

Case 3: Address transmission + Don’t care about expansion

The above two examples of value transfer operations, regardless of whether they expand or not, will not affect the length and capacity of the original slice s1. If weIt is expected that s2 is also modified as well as the original slice s1, then a slice pointer is required to operate based on address transfer.

func main() {
  s1 := make([]int, 0, 5)
  ("s1 slice: ", s1)
  ("s1 slice address: %p len:%d cap:%d\n", &s1, len(s1), cap(s1))
  appendFunc(&s1)
  ("s1 slice: ", s1)
  ("s1 slice expression: ", s1[:5])
}

func appendFunc(s2 *[]int) {
  ("s2 slice address: %p len:%d cap:%d\n", s2, len(*s2), cap(*s2))
  //*s2 = append(*s2, 1, 2, 3)
  *s2 = append(*s2, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
  ("S2 slice address after append: %p len:%d cap:%d\n", s2, len(*s2), cap(*s2))
  ("s2 slice: ", *s2)
}

Output result:

s1 slice:  []
s1 slice address: 0xc00000c030 len:0 cap:5
s2 slice address: 0xc00000c030 len:0 cap:5
S2 slice address after append: 0xc00000c030 len:10 cap:10
s2 slice:  [1 2 3 4 5 6 7 8 9 10]
s1 slice:  [1 2 3 4 5 6 7 8 9 10]
s1 slice expression: [1 2 3 4 5]

All changes will never leave their roots.The address transfer operation always operates the same slice variableAfter the append operation, the length and capacity will change at the same time, and if the capacity expansion is triggered, the pointer to the underlying array will also change at the same time.

Summarize

Slice value transfer operation, append does not trigger expansion, and the value of the underlying array will be modified at the same time, but it will not affect the length and capacity of the original slice; when the expansion is triggered, a copy will be generated, and subsequent modifications will be stripped from the original underlying array and will not affect each other.

If you expect that the original slice will also be modified after modifying the slice, you can useAddress transfer operation, always operate based on the same slice variable.

This is the end of this article about the pitfalls of Go Slice expansion. For more information about Go Slice expansion, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!