Go slice explanation (understanding is the key)

Time:2021-12-29

preface

Through the last blog post, we learned about the use of arrays, but arrays have a fatal defect, that is, fixed size. This feature can not meet our usual development needs, so the go slice was born.

The memory distribution of slices is continuous, so you can treat slices as an array with variable size.

The slice has three field data structures, which contain the metadata of the underlying array that the go language needs to operate. These three fields are the pointer to the underlying array, the number of elements accessed by the slice (i.e. length) and the number of elements allowed to grow by the slice (i.e. capacity). The difference between length and capacity will be further explained later.
Go slice explanation (understanding is the key)

Create and initialize tiles

There are many ways to create slices. Let’s explain them below.

Create slices using make
//A slice with a length and capacity of 5 was created
slice1:=make[[]string,5]
//A slice with a length of 5 and a capacity of 10 was created
slice2:=make[[]string,5,10]

It should be noted that,The size of the underlying array corresponding to the slice is the specified capacity, this must be kept in mind. For example, for the above example, we specifyslice2If the capacity is 10, thenslice2The size of the corresponding underlying array is 10.
Although the size of the underlying array corresponding to the slice created above is 10, you cannot access the elements after the index value of 5. For example, if you run the following code, you will find:

func main() {
    slice := make([]int, 4, 10)
    fmt.Println(slice[2])
    fmt.Println(slice[6])
}

The operation results are as follows:

0
panic: runtime error: index out of range

goroutine 1 [running]:
main.main()
    E:/go-source/go-arr/main.go:19 +0x8d

Process finished with exit code 2
Create slice by slice literal
func main() {
    slice := []int{1, 2, 4, 4}
}

Creating an array is very similar to creating a slice. If you specify a value in [], you will create an array, and vice versa

The slice literal can also specify the size and capacity of the slice, as shown below:

func main() {
    slice := []int{99: 100}
}

The size and capacity of the slice created above are 100, and the value of the 100th initialization element is 100, but in this case, the capacity and length are equal.

Create an empty slice
func main() {
    slice1 := []int{}
    slice2 := make([]int, 0)
}

The empty slice contains 0 elements in the underlying array and does not allocate any storage space. It is useful to represent spatiotemporal slices of empty sets.
Go slice explanation (understanding is the key)

Use of slices

As like as two peas, the slices are identical.

func main() {
    slice1 := []int{1,2,3,4}
    fmt.Println(slice1[1])
}
Slice create slice

A slice is called a slice because it is only a part of the underlying array. See the following code:

func main() {
    slice := []int{10, 20, 30, 40, 50}
    newSlice := slice[1:3]
}

To illustrate the above code, let’s look at the following figure:
Go slice explanation (understanding is the key)

The first slice can see the capacity of all five elements of the underlying array, but the subsequent newslice can’t. For newslice, the capacity of the underlying array is only four elements. Newslice cannot access the part before the first element of the underlying array it points to. Therefore, for newslice, the previous elements do not exist.

Remember that the two slices now share the same underlying array. If one slice modifies the shared part of the underlying array, another slice can also perceive it, run the following code:

func main() {
    slice := []int{10, 20, 30, 40, 50}
    newSlice := slice[1:3]

    slice[1] = 200
    fmt.Println(newSlice[0])
}

The operation results are as follows:

200

Slices can only access elements within their length. Attempting to access an element that exceeds its length will result in a language runtime exception, such as an exception to the abovenewSlice, he can only access elements with indexes 1 and 2 (excluding 3), such as:

func main() {
    slice := []int{10, 20, 30, 40, 50}
    newSlice := slice[1:3]

    fmt.Println(newSlice[3])
}

Run the code and the console will report an error:

panic: runtime error: index out of range

goroutine 1 [running]:
main.main()
    E:/go-source/go-arr/main.go:20 +0x11
Capacity of sub slices

We know that slices can regenerate slices. What is the capacity of sub slices? Let’s test:

func main() {
    slice := make([]int, 2, 10)
    slice1 := slice[1:2]
    fmt.Println(cap(slice1))
}

The console print result is:

9
9

From the results, we can infer that,The capacity of the sub slice is the length of the underlying array minus the start offset of the slice in the underlying arrayFor example, in the above example, the offset value of slice1 is 1 and the size of the underlying array is 10, so subtract the two to get the result 9.

Append element to slice

Go providesappendMethod is used to append elements to the slice, as follows:

func main() {
    slice := make([]int, 2, 10)
    slice1 := slice[1:2]
    slice2 := append(slice1, 1)
    slice2[0] = 10001
    fmt.Println(slice)
    fmt.Println(cap(slice2))
}

The output results are as follows:

[0 10001]
9

At this time, slice, slice1 and slice2 share the underlying array, so as long as a slice changes the value of an index, it will affect all slices. Another thing to note is that the capacity of slice2 is 9. Remember this value.

To illustrate the problem, I change the example to the following code:

func main() {
    slice := make([]int, 2, 10)
    slice1 := slice[1:2]
    slice2 := append(slice1, 1)
    slice2 = append(slice2, 1)
    slice2 = append(slice2, 1)
    slice2 = append(slice2, 1)
    slice2 = append(slice2, 1)
    slice2 = append(slice2, 1)
    slice2 = append(slice2, 1)
    slice2 = append(slice2, 1)
    slice2 = append(slice2, 1)
    slice2 = append(slice2, 1)
    slice2 = append(slice2, 1)
    slice2 = append(slice2, 1)
    slice2[0] = 10001
    fmt.Println(slice)
    fmt.Println(slice1)
    fmt.Println(cap(slice2))
}

At this point, we print the results again, and a magical thing appears:

[0 0]
[0]
18

Although we change the value of 0 position, it does not affect the original slice and slice1. Why? We know that the capacity of the underlying array corresponding to the original slice2 is 9. After a series of append operations, the original underlying array can no longer hold more elements. At this time, go will allocate another piece of memory and copy the memory of the original slice from position 1 to the new memory address, In other words, the underlying array corresponding to slice2 slice and the underlying array corresponding to slice slice are not at the same memory address at all, so when you change the elements in slice2 at this time, it has nothing to do with slice.

In addition, according to the above print results, you should also guess that when the slice capacity is insufficient, go will create a new slice with twice the original slice capacity. In our example, 2 * 9 = 18, which is so rough.

How to specify the capacity when creating sub slices

In the previous example, when we create a sub slice, we do not specify the capacity of the sub slice, so the capacity of the sub slice is equal to the method of calculating the capacity of the sub slice discussed above. How can we manually specify the capacity of the sub slice?

Here we borrow an example from go practice:

func main() {
    source := []string{"Apple", "Orange", "Plum", "Banana", "Grape"}
    slice := source[2:3:4]
    fmt.Println(cap(slice))
}

If you look carefully, the generation method of the above sub slice is different from that of ordinary slice, [] consists of three parts. The first value represents the index position of the starting element of the new slice. In this example, it is 2. The second value represents the starting index position (2) plus the number of elements you want to include (1). The result of 2 + 1 is 3, so the second value is 3. To set the capacity, start from index position 2 and add the number of elements you want to include in the capacity (2) , the third value 4 is obtained. Therefore, the length of the new slice is 1 and the capacity is 2. You must remember that the capacity you specify cannot be larger than the original capacity. Here is the capacity of source. If we add this setting:

func main() {
    source := []string{"Apple", "Orange", "Plum", "Banana", "Grape"}
    slice := source[2:3:10]
    fmt.Println(cap(slice))
}

The operation results are as follows, and an error is reported, ha ha:

panic: runtime error: slice bounds out of range [::10] with capacity 5

goroutine 1 [running]:
main.main()
    E:/learn-go/slice/main.go:7 +0x1d

Iterative slice

For how to iterate slices, we can use the range configuration as follows:

func main() {
    slice:=[]int{1,2,4,6}
    for _, value:=range slice{
        fmt.Println(value)
    }
}

About iterative slicing,You should pay attention to one thing. Take the above example as an example. Value is only a copy of the element in slice, why? Let’s verify this:

func main() {
    slice:=[]int{1,2,4,6}
    for index, value:=range slice{
        fmt.Printf("value[%d],indexAddr:[%X],valueAddr:[%X],sliceAddr:[%X]\n",value,&index,&value,&slice[index])
    }
}

The console print results are as follows:

value[1],indexAddr:[C00000A0B8],valueAddr:[C00000A0D0],sliceAddr:[C000010380]
value[2],indexAddr:[C00000A0B8],valueAddr:[C00000A0D0],sliceAddr:[C000010388]
value[4],indexAddr:[C00000A0B8],valueAddr:[C00000A0D0],sliceAddr:[C000010390]
value[6],indexAddr:[C00000A0B8],valueAddr:[C00000A0D0],sliceAddr:[C000010398]

From the above results, we can see that the addresses of index and value are always the same, so they are always the same variable, but the content of the variable reference address has changed, so during the verification iteration, it can only be a copy of the slice element. Finally, look at the address represented by sliceaddr, which is 8 bytes apart, because on the 64 bit system, The size of each int type is 8 bytes.

Transfer slice between functions

The transfer of slices between functions is also in the form of values, but do you remember the layout of slices given at the beginning of this blog post?
Go slice explanation (understanding is the key)
The slice consists of three parts, including the pointer to the underlying array, the length of the current slice and the capacity of the current slice. Therefore, the slice itself is not large. Let’s test the size of a slice:

func main() {
    slice:=[]int{1,2,4,6}
    fmt.Println(unsafe.Sizeof(slice))
}

The test results are:

24

That is, the slice size is 24 bytes, so when the slice is passed as a parameter, there is almost no performance overhead. Another important point is that the address pointer of the copy generated by the parameter is the same as that of the original slice. Therefore, if you modify the slice in the function, the original slice will be affected. Let’s verify this:

func main() {
    slice:=[]int{1,2,4,6}
    handleSlice(slice)
    fmt.Println(slice)
}

Print results:

[100 2 4 6]

summary

This blog post describes the use and key points of slicing in detail. I hope you will remember that, of course, the most important thing is to understand.

This work adoptsCC agreement, reprint must indicate the author and the link to this article

If you don’t understand something, you can add my QQ: 1174332406 or wechat: togetherforever JS