Overview
In Golang, a slice is very similar to arrays in other languages, yet differs in many ways. As a result, beginners often develop misunderstandings and unwittingly fall into various pitfalls when using slices. This short article first starts from the official Go blog, laying out the slice-related syntax provided by the official documentation. Next, it presents a model for understanding slices using diagrams. Finally, it summarizes and analyzes some special usage scenarios, aiming to provide a clearer profile of slices from multiple perspectives.
If you do not wish to read the lengthy narrative, you can jump directly to the summary at the end.
go-slice-view-derive.png
Author: 木鸟杂记 https://www.qtmuniao.com/2021/01/09/go-slice/, please indicate the source when reposting
Basic Syntax
This section is mainly based on the official Go blog. In Go, slices and arrays are closely related; slices are built on top of arrays but are more flexible. Therefore, in Go, the underlying arrays of slices are rarely used directly. However, to understand slices, we must start with arrays.
Arrays
Arrays in Go are defined by type + length. Unlike C and C++, arrays of different lengths in Go are different types, and the variable name is not a pointer to the first element of the array.
1 | // Several ways to initialize arrays |
To summarize, Go arrays have the following characteristics:
- The length is part of the type, so variables of type
[4]intand[5]intcannot be assigned to each other, nor can they be cast to each other. - An array variable is not a pointer, so passing it as an argument causes a full copy. Of course, you can avoid this copy by using the corresponding pointer type as the parameter type.
It can be seen that due to the shackles of length, the usefulness of Go arrays is greatly limited. Go cannot, like C/C++, convert arrays of arbitrary lengths into pointers to the corresponding type and then perform subscript operations. Of course, Go does not need to do this, because it has a higher-level abstraction—the slice.
Slices
Slices are very common in Go code, but slices are based on arrays underneath:
1 | type slice struct { |
Compared to arrays, slices have the following advantages:
- Flexible operations; as the name suggests, they support powerful slicing operations.
- Free from the restriction of length; when passed as parameters, slices of different lengths can all be passed in the form of
[]T. - When assigning or passing a slice, the entire underlying array is not copied; only the slice struct itself described above is copied.
- With the help of some built-in functions, such as append/copy, they can be easily extended and moved as a whole.
Slice Operations. Using slice operations, you can quickly extract, extend, assign, and move slices.
1 | // Slicing operation, left-closed and right-open; if starting from the beginning or ending at the end, the corresponding index can be omitted |
Parameter Passing. Slices of different lengths and capacities can all be passed in the form of []T.
1 | b := []int{1,2,3,4} |
Related Functions. The main built-in functions related to slices are:
- make for creation
- append for extension
- copy for moving
Let us discuss their characteristics separately.
The signature of the make function when creating slices (it can also be used to create many other built-in structures) is func make([]T, len, cap) []T. This function first creates an array of length cap, then creates a new slice struct pointing to that array, and initializes len and cap according to the parameters.
append modifies the underlying array of the slice, but does not change the original slice; instead, it returns a new slice struct with a new length. Why not modify the original slice in place? Because functions in Go are pass-by-value; of course, this also reflects a certain functional-style preference in Go. Therefore, append(s, 'a', b'') will not modify the slice s itself; to achieve the purpose of modifying the variable s, you need to reassign s: s = append(s, 'a', b'').
Note that when appending, if the capacity (cap) of the underlying array is insufficient, a new array large enough to hold all elements will be created, similar to the underlying mechanism of vector in C++, and the values of the original array will be copied over before appending. If the underlying array of the original slice is no longer referenced by other slice variables, it will be reclaimed during GC.
The copy function is more like syntactic sugar, encapsulating batch assignment to slices into a function. Note that the copy length takes the smaller of the two slices. Moreover, you do not need to worry about overlapping when moving sub-slices of the same slice. For example:
1 | package main |
A common usage scenario for copy is when you need to insert an element into the middle of a slice; use copy to move the fragment after the insertion point backward as a whole.
A Model for Understanding Slices
When first using slices, one often feels that the rules are complex and difficult to remember entirely; so I often wondered if there is a suitable model to capture the essence of slices.
One day, an immature idea suddenly popped up: a slice is a linear read-write view that hides the underlying array. This view avoids the common pointer arithmetic operations in C/C++, because users can derive slices without calculating offsets.
A slice uses only three variables, ptr/cap/len, to depict a window view, where ptr and ptr+cap are the start and end boundaries of the window, and len is the currently visible length of the window. A new view can be extracted by subscripting, and Go will automatically calculate the new ptr/len/cap. All views derived through slice expressions point to the same underlying array.
go slice view
Slice derivation automatically shares the underlying array to avoid array copying and improve efficiency; when appending elements, if the capacity of the underlying array is insufficient, append will automatically create a new array and return a slice view pointing to the new array, while the original slice view still points to the original array.
Slice Usage
This section summarizes some interesting points about using slices.
Zero-value and Empty-value. In Go, all types have a zero-value, which is used as the default value during initialization. The zero-value of a slice is nil.
1 | func add(a []int) []int { // nil can be passed as an argument to the []int slice type |
You can create an empty slice using make, with the same len/cap as the zero-value, but there are also the following small differences. If both are acceptable, nil is recommended.
1 | func main() { |
append Semantics. append first appends elements to the underlying array, then constructs a new slice and returns it. That is to say, even if we do not use the return value, the corresponding values will still be appended to the underlying array.
1 | func main() { |
Generating a Slice from an Array. You can generate a slice s of the desired length from an array a using slice syntax. At this point: the underlying array of s is a. In other words, using slice syntax on an array will not cause the array to be copied.
1 | func main() { |
Modifying the Right Boundary of a Slice View. In the view model proposed above, when performing a slice operation, the left boundary of the newly generated slice changes with the start parameter, but the right boundary remains unchanged, which is the end of the underlying array. If we want to modify its right boundary, we can use the three-parameter slice (Full slice Expression), adding a limited-capacity parameter.
A usage scenario for this feature is that if we want the new slice not to affect the original array when appending, we can modify its right boundary so that when appending, if the capacity is insufficient, a new underlying array is forced to be created.
go-full-slice-view-derive.png
Summary
The core purpose of this article is to propose an easy-to-remember and easy-to-understand model for slices, to break down the ever-changing complexity of slice usage. To summarize, when understanding slices, we can approach it from two levels:
- Underlying data (underlying array)
- Upper view (slice)
The view has three key variables: array pointer (ptr), valid length (len), and view capacity (cap).
Through slice expressions, slices can be generated from arrays and from slices. This operation does not cause copying of array data. Through the append operation, whether array copying is performed depends on the cap of this view, and a view pointing to the new array is returned.
References
- 酷壳 coolshell: Go编程模式:切片,接口,时间和性能
- The Go Blog: Go slices: usage and internals
