Some data structures that slice needs to understand during use

Time:2022-3-1

Src / Runtime / slice Go: slice defines the data structure of slice. From the data structure, slice is very clear. The array pointer points to the underlying array, len represents the slice length, and cap represents the capacity of the underlying array:

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

In the following figure, when using make to create slice, you can specify the length and capacity at the same time. When creating slice, the bottom layer will allocate an array, and the length of the array is the capacity. For example, the structure of slice created by the statement slice: = make ([] int, 5, 10) is as follows:

Some data structures that slice needs to understand during use

  • It can be understood from the above figure that the length of slice is 5, that is, the subscripts slice [0] ~ slice [4] can be used to operate the elements inside. The capacity is 10, which means that when adding new elements to slice later, it is not necessary to reallocate memory, and the reserved memory can be used directly

When using an array to create slice, slice will share some memory with the original array.
For example, the structure of slice created by the statement slice: = array [5:7] is shown in the following figure:

Some data structures that slice needs to understand during use

  • Slicing starts with array [5] and ends with array [7] (excluding array [7], that is, the length of slicing is 2, and the contents behind the array are used as the reserved memory of slicing, that is, the capacity is 5. Array and slicing operations may act on the same block of memory, which is also something to pay attention to in the process of use.

How does slice expand its capacity?

  • When using append to append elements to a slice, if the slice space is insufficient, the slice expansion will be triggered. In fact, a larger memory will be allocated for the expansion. The original slice data will be copied into the new slice, and then the new slice will be returned. After the expansion, the data will be added.
    The following figure shows that when adding another element to a slice with capacity of 5 and length of 5, capacity expansion will occur, as shown in the following figure:

Some data structures that slice needs to understand during use

  • It can be seen from the above figure that the capacity expansion operation only cares about the capacity. The original slice data will be copied to the new slice, and the additional data will be completed by append after the capacity expansion. After the expansion, the length of the new slice is still 5, but the capacity has been increased from 5 to 10, and the data of the original slice has also been copied to the array pointed to by the new slice.

Slice capacity expansion follows the following rules:

  • If the capacity of the original slice is less than 1024, the capacity of the new slice will be doubled;
  • If the capacity of the original slice is greater than or equal to 1024, the capacity of the new slice will be expanded to 1.25 times of the original;

The implementation steps of adding an element to slice using append() are as follows:

  • If slice has enough capacity, add new elements, slice Len + +, return the original slice
  • If the capacity of the original slice is not enough, expand the slice first, and then get a new slice
  • Append the new element to the new slice, slice Len + +, return the new slice.

Will slice be expanded by using copy?

  • When using the copy() built-in function to copy two slices, the data of the source slice will be copied to the array pointed to by the destination slice one by one, and the copy quantity takes the minimum value of the length of the two slices. For example, when a slice with a length of 10 is copied to a slice with a length of 5, 5 elements will be copied. In other words, capacity expansion will not occur during the copy process.

Generating a new slice from an array or slice is a special kind of slice. The newly generated slice does not specify the capacity of the slice. In fact, the capacity of the new slice starts from start to the end of the array.

  • For example, the following two slices have the same length and capacity, and use the same memory address:
    sliceA := make([]int, 5, 10)
    sliceB := sliceA[0:5]

Another way to generate slices based on arrays or slices is to specify slice [start, end, cap], where cap is the capacity of the new slice. Of course, the capacity cannot exceed the actual value of the original slice, as shown below:

sliceA := make([]int, 5, 10) //length = 5; capacity = 10
sliceB := sliceA[0:5] //length = 5; capacity = 10
sliceC := sliceA[0:5:5] //length = 5; capacity = 5

Precautions for using slices:

  • When creating slices, the capacity can be pre allocated according to the actual needs, and the capacity expansion operation in the process of adding can be avoided as far as possible, which is conducive to improving the performance;
  • When slicing copy, you need to judge the number of elements actually copied
  • Be careful to use multiple slices to operate on the same array to prevent read-write conflicts

Finally, I would like to summarize my understanding of slicing. If there is any deficiency, let the discussion area know:

  • Each slice points to an underlying array
  • Each slice saves the length of the current slice and the available capacity of the underlying array
  • Using len() to calculate the slice length does not require traversing the slice
  • Using cap () to calculate slice capacity does not require traversing slices
  • When the slice itself is passed, the whole structure is not passed through the slice itself
  • Using append() to append elements to slices may trigger capacity expansion, and a new slice will be generated after capacity expansion

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