go bottom series - slice bottom implementation

Posted by BTalon on Thu, 09 Dec 2021 22:36:15 +0100

slice

preface

  • Also known as dynamic array, it is realized by array
  • Convenient capacity expansion, transmission, etc

Because of its flexibility, if you do not understand its internal implementation mechanism, you may encounter inexplicable exceptions.

Warm Up

// Topic 1
package main

import "fmt"

func main() {
	var array [10]int
	var slice = array[5:6]
	fmt.Println("length of slice: ", len(slice))
	fmt.Println("capacity of slice: ", cap(slice))
	fmt.Println(&slice[0] == &array[5])
}
// Procedure interpretation:
//		The main function defines a 10 length integer array array
//		Then a slice is defined,
//		Cut the sixth element of the array,
//		Finally, print the length and capacity of slice to determine whether the addresses of the first element of slice and the sixth element of array are equal.

// Reference answer:
//		slice is created according to array and shares storage space with array
//		slice starts at array[5], with length of 1 and capacity of 5
//		slice[0] and array[5] have the same address


// Topic 2
package main

import "fmt"

func AddElement(slice []int, e int) []int {
	return append(slice, e)
}

func main() {
	var slice []int
	slice = append(slice, 1, 2, 3)
	
	newSlice := AddElement(slice, 4)
	fmt.Println(&slice[0] == &newSlice[0])
}
// Procedure interpretation:
// 		The function AddElement() accepts a slice and an element, append s the element into the slice, and returns the slice.
//		Define a slice in the main() function and append three elements to the slice,
//		Then call AddElement() to continue to enter the fourth element into the slice append,
//		At the same time, define a new slice newSlice.
//		Finally, judge whether the new slice and the old slice share a storage space.

// Reference answer:
//		When the append function is executed, it will judge whether the slice capacity can store new elements,
//		If not, it will re apply for storage space,
//		The new storage space will be 2 times or 1.25 times the original (depending on the size of the expanded original space)
//		In this example, the append operation is actually performed twice. For the first time, the space increases to 4,
//		Therefore, the second append will not be expanded, so the old and new slices will share a storage space. The program will output "true"

// Topic 3
package main

import "fmt"

func main() {
	orderLen := 5
	order := make([]uint16, 2 * orderLen)

	pollorder := order[:orderLen:orderLen]
	lockorder := order[orderLen:][:orderLen:orderLen]

	fmt.Println("len(pollorder) = ", len(pollorder))
	fmt.Println("cap(pollorder) = ", cap(pollorder))
	fmt.Println("len(lockorder) = ", len(lockorder))
	fmt.Println("cap(lockorder) = ", cap(lockorder))
}
// Procedure interpretation:
// 		The program is derived from the implementation code of select. A slice order with a length of 10 is defined in the program,
//		pollorder and lockorder are the slices generated by the order[low:high:max] operation on the order slice
//		Finally, the program prints the capacity and length of pollolder and lockorder respectively.

// Reference answer:
//		The order[low:high:max] operation means slicing an order
//		The new slice range is [low, high], and the new slice capacity is max
//		orderLen with twice the length of order
//		The pollorder slice refers to the first half of the order slice, and the lockorder slice refers to the second half of the order slice
//		That is, the original order is divided into two sections. Therefore, the length and capacity of pollorder and lockorder are orderLen, that is, 5

Implementation principle

data structure

  • The array pointer points to the underlying array
  • len is the slice length
  • cap represents the capacity of the underlying array

Create Slice with make

  • When using make to create a Slice, you can specify both length and capacity
  • When creating, the bottom layer will allocate an array, and the length of the array is the capacity
for example

Slice statement: = slice created by make ([] int, 5, 10). The structure is shown in the following figure:

  • The length of the 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 the reserved memory can be used directly without reallocating memory when adding new elements to slice

Create Slice using array

  • When using an array to create Slice, Slice will share some memory with the original array
for example

Slice created by the statement slice = array [5:7], and its structure is shown in the following figure:

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

Slice expansion

  • When using append to append elements to Slice, if Slice space is insufficient, Slice expansion will be triggered
  • The expansion is actually a reconfiguration of a larger memory
  • Copy the original Slice data into the new Slice
  • Then return to the new Slice
  • Add data after capacity expansion
for example

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:

  • Capacity expansion only concerns capacity
  • The original Slice data will be copied to the new Slice
  • Appending data is completed by append after capacity expansion

The expansion capacity shall be selected according to 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 original Slice capacity is greater than or equal to 1024, the new Slice capacity 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 the Slice capacity is sufficient, add the new element, Slice.len + +, and return to 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 + +, and return the new Slice.

Slice Copy

When copying two slices using the copy() built-in function:

  • The data of the source slice is copied one by one to the array pointed to by the destination slice
  • The number of copies is the minimum of the two slice lengths
  • for example
    • When a slice of length 10 is copied to a slice of length 5
    • Five elements will be copied
    • In other words, capacity expansion will not occur during the copy process.

Special slice

  • Slice: = array [start: end] is generally used to generate new slices from arrays or slices
    • This newly generated slice does not specify the capacity of the slice,
    • In fact, the capacity of the new slice is 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]

// There is another way to generate slices from arrays or slices, that is, slices also specify the capacity
// slice[start:end:cap]
// cap is the capacity of the new slice. Of course, the capacity cannot exceed the actual value of the original slice
// As follows:
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

// This slicing method is uncommon and can be seen in the Golang source code, but it is very conducive to the understanding of slicing.

Programming Tips

  • When creating slices, the capacity can be pre allocated according to the actual needs to avoid capacity expansion during the addition process as far as possible, which is conducive to improving performance
  • When slicing copies, 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

Slice summary

  • 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, the time complexity is O(1), and there is no need to traverse the slice
  • The time complexity of using cap() to calculate slice capacity is O(1), and there is no need to traverse slices
  • When a slice is passed through a function, the entire slice is not copied because the slice itself is just a structure
  • Using append() to append elements to slices may trigger capacity expansion, and new slices will be generated after capacity expansion

Topics: Go Back-end