Go Basics: Introduction and use of Slice

This article explains slice from three aspects:

  • Introduction to slice: including the concept of slice, composition and basic operations.
  • The append() function is introduced, and the slice expansion strategy is analyzed from the source level.
  • Sample slice usage, covering everyday use of Slice.

introduce

This section covers the basics of Slice from the following six aspects:

  • The concept of slices
  • Composition of slices
  • Operation of slice
  • Initialization of slices
  • Comparison of slices
  • The zero value of the slice

The concept of slices

Slice represents a variable-length sequence in which each element has the same type.

A slice type is written as []T, where T represents the type of the element in slice.

Slice’s syntax is similar to that of an array, except that there is no fixed length.

Arrays and Slice are closely related. A slice is a lightweight data structure that provides access to subsequences (or all) of array elements. The bottom layer of a slice refers to an array object.

Composition of slices

A slice consists of three parts: pointer, length, and volume.

Pointers to the address of the underlying array element corresponding to the first slice element. Note that the first slice element is not necessarily the first element in the array.

Length corresponds to the number of elements in slice; The length cannot exceed the capacity, which is typically from the start of slice to the end of the underlying data.

The built-in len() and cap() functions return the length and size of slice, respectively.

Underlying data can be shared between multiple slices, and portions of the array referenced may overlap.

Operation of slice

Slice’s slicing operation s[I :j], where 0 ≤ I ≤ j≤ cap(s), is used to create a new slice that references a subsequence of s from the ith element to the j-1 element. The new slice has only J-i elements.

If the index at the I position is omitted, 0 is used instead.

Len (s) is used instead if the index at the j position is omitted

Slicing operations that exceed the cap(s) limit will result in a panic exception, but exceeding len(s) means that the slice has been extended because the new slice is larger in length.

In addition, slicing of strings is similar to slicing of []byte. Both are written as x[m:n], and both return a subsequence of the original sequence of bytes, and both share the previous underlying array, so the operations are constant time. The x[m:n] slicing operation generates a new string for the string.

Because the slice value contains a pointer to the first slice element, passing slice to a function allows the elements of the underlying array to be modified inside the function. In other words, copying a slice simply creates a new Slice alias for the underlying array.

Initialization of slices

The difference in initialization syntax between slice variable S and array variable A. Slice and array literals have similar syntax in that they contain a sequence of initialization elements in parentheses, but do not specify the length of the sequence for slice. This implicitly creates an appropriately sized array, and slice’s pointer points to the underlying array. Just like array literals, slice literals can be initialized sequentially, either by index and element values, or initialized using a hybrid syntax of both styles.

Comparison of slices

Unlike arrays, slices cannot be compared, so we cannot use the == operator to determine whether two slices contain all equal elements. However, the library provides a highly optimized bytes.equal function to determine whether two bytes are Equal ([] bytes), but for other types of slices, we must define our own function to compare each element.

The only valid comparison operation for Slice is to compare to nil.

The zero value of the slice

A zero-valued slice equals nil. A nil slice has no underlying array. A nil slice has a length and capacity of 0, but there are non-nil slices that have a length and capacity of 0, such as []int{} or make([]int, 3)[3:]. As with any nil value, we can use a []int(nil) conversion expression to generate a nil value of the corresponding type slice.

If you need to test if a slice is empty, use ·len(s) == 0· instead of s == nil. A nil value slice behaves like any other slice of 0 length, except for comparison with nil; For example reverse(nil) is also safe. All Go language functions should treat nil-valued slices and zero-length slices the same way, except where the documentation has already made it clear.

The built-in make function creates a slice that specifies the element type, length, and capacity. The capacity part can be omitted, in which case the capacity will be equal to the length.

make([]T, len)
make([]T, len.cap) // equivalent to make([]T, cap)[:len]
Copy the code

At the bottom, make creates an anonymous array variable and returns a slice; The underlying anonymous array variable can only be referenced through the returned slice. In the first, a slice is a view of the entire array. In the second statement, slice references only the first len of the underlying array, but the capacity will contain the entire array. Additional elements are reserved for future growth.

append()

  • Introduction to the append() function
  • Slice expansion strategy source code analysis

Append () is introduced

The built-in append() function appends elements to slice.

slice = append(slice, elem1, elem2)
slice = append(slice, anotherSlice...)
Copy the code

We do not know whether the call to the append() function caused a memory reallocation, so we cannot confirm whether the new slice and the original slice reference the same slice’s underlying array space. Again, we can’t confirm whether actions on the original slice will affect the new slice. Therefore, the result returned by Append () is typically assigned directly to the input slice variable. 支那

Updating the slice variable is necessary not only for calling the append function, but for virtually any operation that might cause changes in length, capacity, or the underlying array. To use slice properly, remember that while the elements of the underlying array are accessed indirectly, the pointer, length, and capacity portions of slice’s corresponding structure itself are accessed directly. From this perspective, slice is not a pure reference type.

Slice Expansion Policy

Here is a snippet of slice expansion policy code from the Runtime package of Go 1.16:

newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
    newcap = cap
} else {
    if old.cap < 1024 {
        newcap = doublecap
    } else {
        // Check 0 < newcap to prevent overflow
        // And avoid infinite loops
        for 0 < newcap && newcap < cap {
            newcap += newcap / 4
        }
        // Overflow detection
        if newcap <= 0 {
            newcap = cap}}}Copy the code

This is a brief introduction to a few key variables:

  • cap:growslice()The input argument to the function that specifies theThe smallest capacity.
  • newcap: After calculation,Capacity after capacity expansion.

From the expansion code, you can see that the expansion policy is as follows:

  1. If the specified minimum capacity is greater than twice the current capacity, the system directly expands the capacity.
  2. Otherwise, if the current capacity is less than 1024, expand the capacity twice.
  3. None of the above is met when the capacity is current capacity1.25timesExponential growth, until greater than the specifiedThe smallest capacity.
  4. If the third capacity expansion method is used, result innewcapOverflows to the specified minimum capacitycap.

use

This summary shows examples of slicing from the following eight aspects:

  • Create a slice
  • Using slice
  • Slice growth
  • Three indexes when creating a slice
  • Iterative slice
  • Multidimensional slice
  • Pass slices between functions
  • Simulation of the stack

Create a slice

make

One way to create slices is to use the built-in make() function. A parameter is passed to specify the length of the slice:

slice := make([]int.5)
Copy the code

If only length is specified, the size of the slice is equal to its length. You can also specify length and capacity separately:

slice := make([]int.3.5)
Copy the code

The length of the underlying array is the specified size, but not all array elements can be accessed after initialization.

Slice literal

Another common way to create slices is to use slice literals:

slice := []int{1.2.3.4.5}
Copy the code

This is similar to creating an array, except that you do not need to specify values in the [] operator. The initial length and capacity are determined based on the number of elements supplied at initialization.

Create slices using indexes

slice := []int{3: 1}
Copy the code

Create an integer slice. Note that the colon is used inside the braces, not the comma. This method initializes the first three elements with zero values, and 1 is assigned to the fourth element.

The difference from an array

slice := []int{1.2.3}
array := [3]int{1.2.3}
Copy the code

If you specify a value in the [] operator, you create an array instead of a slice. Slices are created only if no value is specified.

Nil slice

Nil slices are a common way to create slices in Go. Nil slicing can be used in many standard libraries and built-in functions. Nil slicing is useful when you need to describe a slice that doesn’t exist. For example, when the function asks to return a slice but an exception occurs.

var slice []int
Copy the code

Empty section

With initialization, an empty slice can be created by declaring a slice.

slice := make([]int.0)
slice := []int{}
Copy the code

An empty slice contains zero elements in the underlying array and no storage space is allocated. It is useful to represent a space-time slice of an empty set, for example, when a database query returns zero query results.

Calling the built-in functions append(), len(), and cap() has the same effect whether you use nil slices or empty slices.

Using slice

Create a slice using a slice

slice := []int{1.2.3.4.5}
newSlice := slice[1:3]
Copy the code

Two slices share the same underlying array, but different slices show different parts of the underlying array.

Length and capacity

For slice[I :j] whose underlying array size is K:

  • Length: j - i
  • Capacity:k - i

Modify section Content

newSlice[1] = 9
Copy the code

Assigning 9 to the second element of newSlice (index 1) is also modifying the third element of the original slice (index 2).

Cross-border access

A slice can only access elements within its length. Attempting to access an element beyond its length will result in a language runtime exception. The element associated with the size of the slice can only be used to grow the slice. Before using this element, you must merge it into the length of the slice.

newSlice[3] = 9

// Runtime Exception:
// panic: runtime error: index out of range
Copy the code

Slice growth

One advantage of using slices over arrays is that you can increase the size of slices on demand. The built-in Append () function of the Go language handles all the operational details of increasing the length.

To use append, you need a slice to be manipulated and a value to append. When the append() call returns, a new slice containing the modified results is returned. The function append() always increases the length of a new slice, and the capacity may or may not change, depending on the available capacity of the slice being operated on.

When capacity expansion does not occur

slice := []int{1.2.3.4.5}
newSlice := slice[1:3]
newSlice = append(newSlice, 9)

// slice : [1, 2, 3, 9, 5]
// newSlice : [2, 3, 9]
Copy the code

Because newSlice has additional capacity available in the underlying array, the append operation merges the available elements to the length of the slice and assigns them values. Because it shares the same underlying array as the original slice, the value of the element indexed 3 in slice is also changed.

When capacity expansion occurs

If the underlying array of slices does not have enough available capacity, the append() function creates a new underlying array, copies the existing referenced values into the new array, and appends the new values.

slice := []int{1.2.3.4}
newSlice := append(slice, 5)

// slice : [1, 2, 3, 4]
// newSlice : [1, 2, 3, 4, 5]
Copy the code

See section Append () for details on expansion policies.

Three indexes when creating a slice

When creating slices, you can also use a third index option that we didn’t mention earlier. A third index can be used to control the size of new slices. The goal is not to increase capacity, but to limit it. Allowing the capacity of new slices to be limited provides some protection for the underlying array and provides better control over append operations.

source := []string{"Apple"."Orange"."Plum"."Banana"."Grape"}
slice := source[2:3:4]
Copy the code
  • Length:j - i3 minus 2 is 1
  • Capacity:k - i4 minus 2 is 2

If you try to set more capacity than is available, you get a language runtime error.

slice := source[2:3:6]
// Runtime Error:
// panic: runtime error: slice bounds out of range
Copy the code

As we discussed earlier, the built-in function append() uses the available capacity first. Once no capacity is available, a new underlying array is allocated. This makes it easy to forget that slices are sharing the same underlying array. When this happens, modifying the slice is likely to cause random and strange problems. Modification of slice contents can affect multiple slices, but it is difficult to find the cause of the problem.

If you set the size and length of the slice to be the same when creating the slice, you can force the first append operation of the new slice to create a new underlying array, separate from the original. After the new slice is separated from the original underlying array, subsequent modifications can be made safely.

source := []string{"Apple"."Orange"."Plum"."Banana"."Grape"}
slice := source[2:3:3]
slice = append(slice, "Kiwi")
Copy the code

If the third index is not added, appending Kiwi to slice will change the value Banana of the element with index 3 in the underlying array, since all remaining capacity belongs to slice. However, in Listing 4-36 we limit the size of slice to 1. When we first call Append to slice, we create a new underlying array containing two elements, copy Plum, append Kiwi, and return a new slice referencing the underlying array.

Because the new slice has its own underlying array, problems are eliminated. We can continue to add fruit to the new slice without worrying about accidentally modifying other slices

Iterative slice

Since a slice is a set, you can iterate over its elements. The Go language has a special keyword range, which can be used with the keyword for to iterate over the elements in the slice.

slice := []int{10.20.30.40}
for index, value := range slice {
	fmt.Printf("Index: %d Value: %d\n", index, value)
}

/ / output:
// Index: 0 Value: 10
// Index: 1 Value: 20
// Index: 2 Value: 30
// Index: 3 Value: 40
Copy the code

When iterating over slices, the keyword range returns two values. The first value is the index position to which the current iteration is based, and the second value is a copy of the element value corresponding to that position.

It’s important to note that Range creates a copy of each element, rather than returning a reference to that element directly. Using the address of the value variable as a pointer to each element causes an error.

slice := []int{10.20.30.40}
for index, value := range slice {
	fmt.Printf("Value: %d Value-Addr: %X ElemAddr: %X\n", value, &value, &slice[index])
}

/ / output:
// Value: 10 Value-Addr: 10500168 ElemAddr: 1052E100
// Value: 20 Value-Addr: 10500168 ElemAddr: 1052E104
// Value: 30 Value-Addr: 10500168 ElemAddr: 1052E108
// Value: 40 Value-Addr: 10500168 ElemAddr: 1052E10C
Copy the code

Since the variable returned by iteration is a new variable assigned in sequence according to the slices during iteration, the address of value is always the same. To get the address of each element, you can use slice variables and index values.

If you do not need an index value, you can use placeholder characters to ignore it.

slice := []int{10.20.30.40}
for _, value := range slice {
	fmt.Printf("Value: %d\n", value)
}
/ / output:
// Value: 10
// Value: 20
// Value: 30
Copy the code

The keyword range always iterates from the slice header. If you want more control over iterations, you can still use the traditional for loop.

slice := []int{10.20.30.40}
for index := 2; index < len(slice); index++ {
	fmt.Printf("Index: %d Value: %d\n", index, slice[index])
}
/ / output:
// Index: 2 Value: 30
Copy the code

There are two special built-in functions len() and cap() that work with arrays, slicing, and channels. For slices, len() returns the length of the slice and cap() returns the capacity of the slice.

Multidimensional slice

slice := [][]int{{10}, {100.200}}
slice[0] = append(slice[0].20)
Copy the code

The append function in Go handles appends in a simple way: grow slices and assign a new integer slice to the first element of the outer slice.

Pass slices between functions

Passing slices between functions means passing slices between functions as values. Due to the small size of slices, the cost of copying and passing slices between functions is also low. Let’s create a large slice and pass this slice as a value to the function foo().

slice := make([]int.1e6)
slice = foo(slice)
func foo(slice []int) []int {}
Copy the code

On a machine with a 64-bit architecture, a slice requires 24 bytes of memory: 8 bytes for the pointer field and 8 bytes for the length and capacity fields. Since the data associated with the slice is contained in the underlying array and does not belong to the slice itself, copying the slice to any function does not affect the size of the underlying array. Only the slice itself is copied, not the underlying array.

Simulation of the stack

We can simulate the loading and unloading of a stack using the append() function and the slice operation.

stack := make([]int.0)

stack = append(stack, 1)
stack = append(stack, 2)
stack = append(stack, 3)
stack = append(stack, 4)

for len(stack) > 0 {
    top := stack[len(stack) - 1]
    stack = stack[:len(stack) - 1]
    fmt.Printf("%v, ", top)
}

/ / output:
// 4, 3, 2, 1,
Copy the code