An overview of the

Slice in Golang is so similar to arrays in other languages, but with so many differences, that beginners can easily misunderstand and fall into all sorts of traps when using it. This article, starting from the official Go blog, explains the official syntax of slice. Secondly, a model to understand slice is presented in graphical form. Finally, we summarize and analyze some specific usage cases in order to have a clearer profile of Slice from multiple angles.

If you don’t want to see the tedious narrative process, you can jump to the final summary to see the summary.

Author: Greenwood Birdwww.qtmuniao.com/2021/01/09/…Please indicate the source

The basic grammar

This section is mainly from Go’s official blog. In Go, slicing and array are companion, and slicing is array-based but more flexible, so arrays as the underlying layer of slicing are rarely used in Go. But to understand slicing, start with arrays.

Array

Arrays in Go consist of type + length. Different from C and C++, arrays of different lengths in Go are of different types, and variable names are not Pointers to the address at the beginning of the array.

// Several ways to initialize an array
var a [4]int             // Variable A is of type [4]int is a type, and each element is automatically initialized to a zero-value of int.
b := [5]int{1.2.3.4}     // the variable b of type [5]int is a different type from [4]int, and b[4] is automatically initialized to zero of int
c := [...]int{1.2.3.4.5} // the variable c is automatically derived as [5]int, the same as b

func echo(x [4]int) {
  fmt.Println(x)
}

echo(a)         When echo is called, all elements in a are copied, because Go calls pass values
echo(b)         // error
echo(([4]int)c) // error
Copy the code

To summarize, arrays of Go have the following characteristics:

  1. The length is part of the type, so[4]int[5]int Variables of type cannot be assigned to each other, nor can they be strong-spun.
  2. Array variables are not Pointers, so passing them as arguments causes a full copy. Of course, you can avoid this copying by using the corresponding pointer type as the parameter type.

As you can see, the Go array’s usefulness is greatly limited by the length constraint. Like C/C++, Go cannot be converted to a pointer to the corresponding type for subscripting. Of course, Go doesn’t need to do that either, because it has a higher abstraction — slicing.

Slices

Slicing is common in Go code, but the underlying slicing is based on arrays:

type slice struct {
    array unsafe.Pointer // A pointer to the underlying array; Yes, golang also has Pointers
    len   int            // Slice length
    cap   int            // The length of the underlying array
}

// Several initialization methods for slices
s0 := make([]byte.5)       // Use the make function where len = cap = 5 and each element is initialized to a zero-value of byte
s1 := []byte{0.0.0.0.0} Len = cap = 5
var s2 []byte               // automatically initialized to slice's zero-value: nil

// Make specifies len and cap. Len <= cap
s3 := make([]byte.0.5) Len = 0, cap = 5
s4 := make([]byte.5.5) // make([]byte, 5)
Copy the code

Slicing has the following advantages over arrays:

  1. Flexible operation, as the name implies, support powerful slicing operations.
  2. The length limit is removed, and slices of different lengths can be used in parameter transmission[]TFormal transfer.
  3. Slice assignment and parameter passing do not copy the entire underlying array, only the slice structure itself.
  4. Some built-in functions, such as Append/Copy, make it easy to expand and move the whole thing around.

Slice operation. Slice operations can be used to quickly intercept, expand, assign, and move slices.

// Intercept operation, left close right open; If it begins at the beginning or ends at the end, the corresponding subscript can be omitted
// The new slice shares the underlying array with the original slice, thus avoiding element copying
b := []byte{'g'.'o'.'l'.'a'.'n'.'g'}
b1 := b[1:4] // b1 == []byte{'o', 'l', 'a'}
b2 := b[:2]  // b2 == []byte{'g', 'o'}
b3 := b[2:]  // b3 == []byte{'l', 'a', 'n', 'g'}
b4 := b[:]   // b4 == b

// To extend operations, use the append function
// May cause a reallocation of the underlying array, as discussed below
// equivalent to b = append(b, []byte{',', 'h', 'I '}...)
b = append(b, ', '.'h'.'i') / / b now {' g ', 'o', 'l', 'a', 'n', 'g' and ', ', 'h', 'I'}

// Copy is used for assignment
copy(b[:2], []byte{'e'.'r'})  / / b now {' e ', 'r', 'l', 'a', 'n', 'g' and ', ', 'h', 'I'}

// Copy is required for moving operations
copy(b[2:], b[6:)Len (DST), len(SRC))
b = b[:5]           // b is now {'e', 'r', ',' h', 'I '}
Copy the code

Parameter passing. Slices of different lengths and capacities can be transmitted in the form of []T.

b := []int{1.2.3.4}
c := []int{1.2.3.4.5} 

func echo(x []int) {
  fmt.Println(x)
}

echo(b) // When the argument is passed, a new slice structure is generated that shares the underlying array with the same len and cap
echo(c)
Copy the code

Correlation function. The built-in functions related to slicing mainly include:

  1. Make for creation
  2. Append for extension
  3. Copy for movement

The following are their characteristics.

The make function is signed func make([]T, len, cap) []T when creating slices (which can also be used to create many other built-in constructs). The function first creates a Cap length array, then creates a new slice structure that points to the array and initializes len and CAP based on the arguments.

Append, after modifying the underlying array of slices, does not change the original slice, but returns a new slice structure with a new length. Why not modify the original slice in situ? Because the function in Go is passing values, of course, this also reflects the preference of some functional thinking in Go. Therefore, append(s, ‘a’, b’) does not modify the slice s itself, it needs to reassign s: s = append(s, ‘a’, b’) to modify the variable s.

Note that when append is used, if the underlying array capacity (CAP) is insufficient, it will create an array that is large enough to hold all the elements, copy the values of the original array, and then append. The original slice underlying array is reclaimed during GC if no other slice variables are referenced.

The copy function is more like a syntactic sugar, encapsulating the batch assignment of slices as a function, noting that the copy length is the smaller of the two slices. In addition, there is no need to worry about coverage when the sub-sections of the same section move, for example:

package main

import (
	"fmt"
)

// Intuitionistic copy function implementation
// However, this implementation will cause overwriting when the sub-slices of the same slice are copied
// Therefore copy should be implemented with extra space or copy from back to front
func myCopy(dst, src []int) {
	l := len(dst)
	if len(src) < l {
		l = len(src)
	}
	
	for i := 0; i < l; i++ {
		dst[i] = src[i]
	}
}

func main(a) {
	a := []int{0.1.3.4.5.6}
	
	copy(a[3:], a[2:)// a = [0 1 3 3 4 5]
	// myCopy(a[3:], a[2:]) // a = [0 1 3 3 3 3]
	fmt.Println(a)
}
Copy the code

A common use of copy is to use copy to move the entire fragment after the insertion point back when an element needs to be inserted into the middle of the slice.

Slice the model

At the beginning of slice, it is often felt that its rules are complicated and difficult to remember; So I often wondered if there was a proper model to describe the nature of slices.

One day it occurred to me that slicing is a linear read-write view that hides the underlying array. Slicing is a view that bypasses pointer operations that are common in C/C++ because users can derive from slicing to avoid calculating offsets.

Slice only uses three variables PTR/Cap/Len to depict a window view, where PTR and PTR + Cap are the start and end limits of the window, and Len is the visible length of the current window. A new view can be cut out by subscript, Go automatically evaluates the new PTR /len/cap, and all views derived from slicing expressions point to the same underlying array.

Slice derivation automatically shares the underlying array to avoid array copying and improve efficiency. When appending an element, if the underlying array is not large enough, AppEnd automatically creates a new array and returns a slice view that points to the new array, while the original slice view still points to the original array.

Slice using

This section summarizes some of the interesting aspects of slice usage.

Zero-value and empty-value. All types in GO have a zero value as the default value at initialization. The zero value of slice is nil.

func add(a []int) []int { // nil can be passed as an argument to []int slice type
	return append(a, 0.1.2)}func main(a) {
	fmt.Println(add(nil)) / / [0, 1, 2]
}
Copy the code

Make can create an empty slice with len/cap identical to zero, but with the following minor differences: nil is recommended if both are used.

func main(a) {
	a := make([]int.0)
	var b []int
	
	fmt.Println(a, len(a), cap(a)) / / [] 0 0
	fmt.Printf("%#v\n", a)         // []int{}
	fmt.Println(a==nil)            // false
	
	fmt.Println(b, len(b), cap(b)) / / [] 0 0
  fmt.Printf("%#v\n", b)         // []int(nil)
	fmt.Println(b==nil)            // true
}
Copy the code

Append semantics. Append first appends elements to the underlying array and then constructs a new slice return. That is, even if we don’t use the return value, the corresponding value is appended to the underlying array.

func main(a) {
	a := make([]int.0.5)
	_ = append(a, 0.1.2)
	fmt.Println(a)     / / []
	fmt.Println(a[:5]) // [0 1 2 0 0]; You can see the appended value by slicing the expression and increasing the window length
  fmt.Println(a[:6]) / / panic; The length is out of bounds
}
Copy the code

Generate slice from array. You can use the slicing syntax to generate the desired length of slice S from array A, where: the underlying array of S is A. In other words, slicing an array does not make a copy of the array.

func main(a) {
	a := [7]int{1.2.3}
	s := a[:4]
	fmt.Println(s) // [1 2 3 0]
	
	a[3] = 4       // If a is a, s is a
	fmt.Println(s) // [1 2 3 4]
}
Copy the code

Modify the right edge of the view when slicing. In the view model proposed above, when slicing is carried out, the left boundary of the newly generated slice will change with the start parameter, but the right boundary remains unchanged, that is, the end of the underlying array. If we want to modify the right bound, we can add a limited-capacity parameter through Full slice Expression.

One use scenario for this feature is that if we want the new slice to have no effect on the original array at append, we can modify its right bound so that cap does not force the generation of a new underlying array at Append.

summary

The core purpose of this article is to propose a slice model that is easy to remember and understand to unravel the ever-changing complexity of slice usage. To summarize, there are two aspects to understanding slice:

  1. Underlying data (underlying array)
  2. Upper view (slice)

The view has three key variables, array pointer (PTR), effective length (len), and view capacity (CAP).

Slice expression allows you to generate slices from an array and from a slice without copying array data. Append returns a view pointing to the new array depending on the cap of the view.

reference

  1. Coolshell: Go Programming mode: slicing, interface, timing and performance
  2. The Go Blog: Go Slices: Usage and Internals

First published on the public account “Miscellaneous Notes of Wooden Birds”