Go Language [Data Structure] Slice

Keywords: Go

Section

brief introduction

Simply put, slicing is a simplified version of a dynamic array. The length of the Go array cannot be changed, and the length of the slice is not fixed, so the length of the slice is naturally not a component of the type. Although arrays have something to do with them, the types and operations of arrays are not flexible enough, so arrays are not used much in Go code. Slicing is widely used. Understanding the principle and usage of slicing is a necessary skill for Go programmers. Memory copy of real data when append and copy functions are performed
 

Initialization

package main

import "fmt"

func main() {
	// The first way to compare arrays does not specify size
	var slice1 = [] int{1,2,3,4}
	// The second way
	slice2 := [] int{1,2,3,4}
	// The third way to make empty slices
	var slice3 []int = make([]int,3,5)
	// The fourth way is to abbreviate len=3 cap=5
	slice4 := make([]int,3,5)

	fmt.Println(slice1)
	fmt.Println(slice2)
	fmt.Println(slice3)
	fmt.Println(slice4)
}

//[1 2 3 4]
//[1 2 3 4]
//[0 0 0]
//[0 0 0]

 

data structure

Let's first look at the structure definition of the slice, reflect.SliceHeader:

type SliceHeader struct {
	// When pointing to the array memory address assignment, the array address is copied.
	Data uintptr
	// length
	Len  int
	// Application space
	Cap  int
}

You can see that the beginning of the slice is the same as the Go string, but the slice has an additional Cap member representing the maximum capacity of the memory space the slice points to (the number of corresponding elements, not bytes). The following figure shows the memory structure corresponding to the two slices x: =[] int {2,3,5,7,11} and y: = x [1:3].

 

 

Assignment, slicing and copy

Assignment is equivalent to copying a variable in the SliceHeader structure without copying the real data. Data and the initial slice point to the same memory address.

package main

import "fmt"

func main() {
	a := [] int{1,2,3,4}
	b := a
	// Data ptr points to the same memory address
	fmt.Printf("a[0] %p\n",&a[0])
	fmt.Printf("b[0] %p\n",&b[0])
	// Modified element values interact
	b[0] = 10
	fmt.Println(a)
	fmt.Println(b)
}

//a[0] 0xc000054140
//b[0] 0xc000054140
//[10 2 3 4]
//[10 2 3 4]

When slicing, the newly generated slice Data points to the memory address of the element at the initial slice location, and when element values are modified, they interact with each other.

package main

import "fmt"

func main() {
	a := [] int{1,2,3,4}
	b := a[:]
	// Data ptr points to the memory address of the first element of a in full slicing
	fmt.Printf("a[0] %p\n",&a[0])
	fmt.Printf("b[0] %p\n",&b[0])
	// Modified element values interact
	b[0] = 10
	fmt.Println(a)
	fmt.Println(b)
}

//a[0] 0xc000054140
//b[0] 0xc000054140
//[10 2 3 4]
//[10 2 3 4] 

When the copy function is operated on, the real Data is copied in memory, and the new sliced Data points to the new address.

package main

import "fmt"

func main() {
	a := [] int{1,2,3,4}
	b := make([]int, len(a), cap(a))
	copy(b,a)
	// New memory address pointed to by Data ptr
	fmt.Printf("a[0] %p\n",&a[0])
	fmt.Printf("b[0] %p\n",&b[0])
	// Modified element values do not affect each other
	b[0] = 10
	fmt.Println(a)
	fmt.Println(b)
}

//a[0] 0xc000054140
//b[0] 0xc000054160
//[1 2 3 4]
//[10 2 3 4]

 

Additive elements

The built-in generic function append adds N elements to the end of the slice:

package main

import "fmt"

func main() {
	var a []int
	// Adding an element
	a = append(a, 1)
	// Adding multiple elements, handwritten unpacking
	a = append(a, 1, 2, 3)
	// Add a slice. The slice needs unpacking.
	a = append(a, []int{1,2,3}...) 

	fmt.Println(a)
}

//[1 1 2 3 1 2 3]

However, it should be noted that under the condition of insufficient capacity, append operation will lead to the reallocation of memory, which may result in huge cost of memory allocation and data replication. Even if the capacity is sufficient, the slice itself needs to be updated with the return value of the append function, because the length of the new slice has changed.

In addition to appending at the end of the slice, we can add elements at the beginning of the slice:

package main

import "fmt"

func main() {
	var a = []int{1,2,3}
	// Add an element at the beginning
	a = append([]int{0}, a...)
	// Add a slice at the beginning
	a = append([]int{-3,-2,-1}, a...) 

	fmt.Println(a)
}

//[-3 -2 -1 0 1 2 3]

At the beginning, memory reallocation is usually caused, and all existing elements are copied once. Therefore, the performance of adding elements from the beginning of the slice is generally much worse than that of adding elements from the tail.

Because the append function returns a new slice, that is, it supports chain operations. We can combine multiple append operations to insert elements in the middle of the slice:

package main

import "fmt"

func main() {
	var a []int
	// Insert x at the first position
	a = append(a[:i], append([]int{x}, a[i:]...)...)
	// Insert slices at the first position
	a = append(a[:i], append([]int{1,2,3}, a[i:]...)...)
}

//[-3 -2 -1 0 1 2 3]

 

Appnd and Memory Address Change

When pointing to an append operation, the real data of the slice is copied in memory without affecting the initial slice.

package main

import "fmt"

func main() {
	a := [] int{1,2,3,4}
	b := append(a, 5)
	// Data ptr points to a new memory address when append is executed
	fmt.Printf("a[0] %p\n",&a[0])
	fmt.Printf("b[0] %p\n",&b[0])
	// Modified element values do not affect each other
	b[0] = 10
	fmt.Println(a)
	fmt.Println(b)
}

//a[0] 0xc000054140
//b[0] 0xc00006e100
//[1 2 3 4]
//[10 2 3 4 5]

  

Delete elements

Delete using the combination of slicing and append operations

package main

import "fmt"

func main() {
	a := []int{1, 2, 3, 4, 5}
	// Delete the tail element
	a = a[:len(a)-1]
	fmt.Println(a)
	// Delete a header element
	a = a[1:len(a)]
	fmt.Println(a)
	// Delete the middle element
	a = append(a[:1],a[2:]...)
	fmt.Println(a)
}

//[1 2 3 4]
//[2 3 4]
//[2 4]

 

Function reference

Slices are passed as parameters, which, consistent with assignments, only copies the variables in the structure, and Data points to the same address.

package main

import "fmt"

func change(list []int){
	// Copy the memory address pointed to by Data ptr
	fmt.Printf("list %p\n",&list[0])
	// Modify slices
	list[0] = 100
}

func main() {
	list := [] int{1,2,3,4}
	fmt.Printf("list %p\n",&list[0])
	change(list)
	// slice affected
	fmt.Println(list)
}

//list 0xc000054140
//list 0xc000054140
//[100 2 3 4]

Posted by techmeister on Sun, 15 Sep 2019 02:47:49 -0700