Slices
In Go, arrays and slices look almost identical, but they have significant functional differences. Arrays are fixed-length data structures whose length cannot be changed once specified, while slices are of variable length and will automatically expand when capacity is insufficient.
Arrays
If you know the length of data to store in advance and there will be no need for expansion during use, you can consider using arrays. Arrays in Go are value types, not references, and are not pointers to the first element.
TIP
As value types, when an array is passed as a parameter to a function, the entire array is copied because Go functions pass by value.
Initialization
When declaring an array, the length must be a constant, not a variable. You cannot declare a variable and then use that variable as the array length value.
// Correct example
var a [5]int
// Incorrect example
l := 1
var b [l]intLet's initialize an integer array of length 5 first:
var nums [5]intYou can also initialize with elements:
nums := [5]int{1, 2, 3}You can let the compiler infer the length:
nums := [...]int{1, 2, 3, 4, 5} // equivalent to nums := [5]int{1, 2, 3, 4, 5}, the ellipsis must exist, otherwise it generates a slice, not an arrayYou can also get a pointer through the new function:
nums := new([5]int)All of the above methods allocate a fixed-size memory for nums. The only difference is that the last one gets a pointer value.
When initializing an array, it's important to note that the length must be a constant expression, otherwise it will not compile. A constant expression is one whose final result is a constant. Incorrect example:
length := 5 // This is a variable
var nums [length]intlength is a variable, so it cannot be used to initialize an array length. Here is the correct example:
const length = 5
var nums [length]int // constant
var nums2 [length + 1]int // constant expression
var nums3 [(1 + 2 + 3) * 5]int // constant expression
var nums4 [5]int // most commonUsage
As long as you have the array name and index, you can access the corresponding element in the array.
fmt.Println(nums[0])Similarly, you can also modify array elements:
nums[0] = 1You can also use the built-in function len to access the number of array elements:
len(nums)Use the built-in function cap to access the array capacity. The array's capacity equals its length. Capacity is meaningful only for slices.
cap(nums)Slicing
The format for slicing an array is arr[startIndex:endIndex], and the sliced range is left-closed right-open. And after slicing, the array becomes a slice type. Example:
nums := [5]int{1, 2, 3, 4, 5}
nums[:] // sub-slice range [0,5) -> [1 2 3 4 5]
nums[1:] // sub-slice range [1,5) -> [2 3 4 5]
nums[:5] // sub-slice range [0,5) -> [1 2 3 4 5]
nums[2:3] // sub-slice range [2,3) -> [3]
nums[1:3] // sub-slice range [1,3) -> [2 3]func main() {
arr := [5]int{1, 2, 3, 4, 5}
fmt.Printf("%T\n", arr)
fmt.Printf("%T\n", arr[1:2])
}Output:
[5]int
[]intTo convert an array to a slice type, simply slice without parameters. The converted slice points to the same memory as the original array. Modifying the slice will cause changes to the original array.
func main() {
arr := [5]int{1, 2, 3, 4, 5}
slice := arr[:]
slice[0] = 0
fmt.Printf("array: %v\n", arr)
fmt.Printf("slice: %v\n", slice)
}Output:
array: [0 2 3 4 5]
slice: [0 2 3 4 5]If you want to modify the converted slice, it is recommended to use the following method:
func main() {
arr := [5]int{1, 2, 3, 4, 5}
slice := slices.Clone(arr[:])
slice[0] = 0
fmt.Printf("array: %v\n", arr)
fmt.Printf("slice: %v\n", slice)
}Output:
array: [1 2 3 4 5]
slice: [0 2 3 4 5]Slices
Slices are much more widely used in Go than arrays. They are used to store data of unknown length, and elements may be frequently inserted and deleted during use.
Initialization
There are several ways to initialize a slice:
var nums []int // value
nums := []int{1, 2, 3} // value
nums := make([]int, 0, 0) // value
nums := new([]int) // pointerAs you can see, the difference between slices and arrays is only that the initialization length is missing. In general, it is recommended to use make to create an empty slice. For slices, the make function accepts three parameters: type, length, and capacity. Let's explain the difference between length and capacity with an example. Suppose there is a bucket of water that is not full. The height of the bucket is its capacity, representing the total amount of water it can hold. The height of the water in the bucket represents the length. The water height must be less than or equal to the bucket height, otherwise the water would overflow. Therefore, the length of a slice represents the number of elements in the slice, and the capacity of a slice represents the total number of elements the slice can hold. The biggest difference between slices and arrays is that the slice's capacity will automatically expand, while the array's will not. For more details, visit Reference Manual - Length and Capacity.
TIP
The underlying implementation of slices is still arrays. Slices are reference types and can be simply understood as pointers to the underlying array (essentially, a slice in Go is a struct containing a pointer to the underlying array, a length value, and a capacity value). Therefore, when a slice is passed as a function parameter, the underlying array is not copied. Modifications to the passed slice within the function will be reflected in the original slice.
When declaring a slice with var nums []int, the default value is nil, so no memory is allocated. When using make for initialization, it is recommended to pre-allocate sufficient capacity, which can effectively reduce memory consumption during subsequent expansions.
Usage
The basic usage of slices is exactly the same as arrays. The only difference is that slices can dynamically change length. Let's look at a few examples.
Slices can perform many operations through the append function. The function signature is as follows: slice is the target slice to add elements to, and elems are the elements to add. The return value is the slice after adding elements.
func append(slice []Type, elems ...Type) []TypeFirst, create an empty slice with length 0 and capacity 0, then insert some elements at the end, and finally output the length and capacity.
nums := make([]int, 0, 0)
nums = append(nums, 1, 2, 3, 4, 5, 6, 7)
fmt.Println(len(nums), cap(nums)) // 7 8 - as you can see, length and capacity are not the same.The size of the reserved buffer for a new slice follows a certain pattern. Before golang version 1.18, most online articles described the slice growth strategy as follows: When the original slice capacity is less than 1024, the new slice capacity becomes twice the original; when the original slice capacity exceeds 1024, the new slice capacity becomes 1.25 times the original. After version 1.18, the slice growth strategy changed to: When the original slice capacity (oldcap) is less than 256, the new slice capacity (newcap) is twice the original; when the original slice capacity exceeds 256, newcap = oldcap + (oldcap + 3*256)/4
Inserting Elements
Inserting slice elements also requires using the append function. Given the following slice:
nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}Insert elements at the beginning:
nums = append([]int{-1, 0}, nums...)
fmt.Println(nums) // [-1 0 1 2 3 4 5 6 7 8 9 10]Insert elements at index i:
nums = append(nums[:i+1], append([]int{999, 999}, nums[i+1:]...)...)
fmt.Println(nums) // i=3, [1 2 3 4 999 999 5 6 7 8 9 10]Insert elements at the end, which is the most basic use of append:
nums = append(nums, 99, 100)
fmt.Println(nums) // [1 2 3 4 5 6 7 8 9 10 99 100]Deleting Elements
Deleting slice elements requires using the append function. Given the following slice:
nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}Delete n elements from the beginning:
nums = nums[n:]
fmt.Println(nums) // n=3 [4 5 6 7 8 9 10]Delete n elements from the end:
nums = nums[:len(nums)-n]
fmt.Println(nums) // n=3 [1 2 3 4 5 6 7]Delete n elements starting from index i:
nums = append(nums[:i], nums[i+n:]...)
fmt.Println(nums)// i=2, n=3, [1 2 6 7 8 9 10]Delete all elements:
nums = nums[:0]
fmt.Println(nums) // []Copying
When copying a slice, ensure that the target slice has sufficient length. For example:
func main() {
dest := make([]int, 0)
src := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
fmt.Println(src, dest)
fmt.Println(copy(dest, src))
fmt.Println(src, dest)
}[1 2 3 4 5 6 7 8 9] []
0
[1 2 3 4 5 6 7 8 9] []Change the length to 10, output:
[1 2 3 4 5 6 7 8 9] [0 0 0 0 0 0 0 0 0 0]
9
[1 2 3 4 5 6 7 8 9] [1 2 3 4 5 6 7 8 9 0]Traversal
Traversal of slices is exactly the same as arrays. Using for loop:
func main() {
slice := []int{1, 2, 3, 4, 5, 7, 8, 9}
for i := 0; i < len(slice); i++ {
fmt.Println(slice[i])
}
}Using for range loop:
func main() {
slice := []int{1, 2, 3, 4, 5, 7, 8, 9}
for index, val := range slice {
fmt.Println(index, val)
}
}Multi-dimensional Slices
Let's look at the example below. The official documentation also explains this: Effective Go - Two-dimensional Slices
var nums [5][5]int
for _, num := range nums {
fmt.Println(num)
}
fmt.Println()
slices := make([][]int, 5)
for _, slice := range slices {
fmt.Println(slice)
}Output:
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[]
[]
[]
[]
[]As you can see, the internal structures of two-dimensional arrays and slices are different. When an array is initialized, both the first and second dimensions' lengths are fixed. However, the length of slices is not fixed. Each slice within a slice may have a different length, so they must be initialized separately. Simply modify the slice initialization part to:
slices := make([][]int, 5)
for i := 0; i < len(slices); i++ {
slices[i] = make([]int, 5)
}Final output:
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]Extended Slice Expression
TIP
Only slices can use extended slice expressions
Both slices and arrays can use simple expressions for slicing, but only slices can use extended slice expressions. This feature was added in Go 1.2 to solve the read/write problem when slices share the underlying array. The main format is as follows, and must satisfy the relationship low <= high <= max <= cap. The capacity of a slice sliced using extended expression is max - low.
slice[low:high:max]low and high retain their original meanings, while the additional max refers to the maximum capacity. For example, in the following example where max is omitted, the capacity of s2 is cap(s1) - low:
s1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9} // cap = 9
s2 := s1[3:4] // cap = 9 - 3 = 6However, there is an obvious problem: s1 and s2 share the same underlying array. When reading or writing to s2, it may affect s1's data. The following code is such a case:
s1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9} // cap = 9
s2 := s1[3:4] // cap = 9 - 3 = 6
s2 = append(s2, 1) // Add new element, since capacity is 6, no expansion happens, directly modifies the underlying array
fmt.Println(s2)
fmt.Println(s1)The output:
[4 1]
[1 2 3 4 1 6 7 8 9]As you can see, adding elements to s2 also modified s1. The extended slice expression was created to solve such problems. You can solve this problem by making a small modification:
func main() {
s1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9} // cap = 9
s2 := s1[3:4:4] // cap = 4 - 3 = 1
s2 = append(s2, 1) // Insufficient capacity, allocate new underlying array
fmt.Println(s2)
fmt.Println(s1)
}Now the result is normal:
[4 1]
[1 2 3 4 5 6 7 8 9]clear
Go 1.21 added the clear built-in function. Clear sets all values in the slice to zero values.
package main
import (
"fmt"
)
func main() {
s := []int{1, 2, 3, 4}
clear(s)
fmt.Println(s)
}Output:
[0 0 0 0]If you want to empty the slice:
func main() {
s := []int{1, 2, 3, 4}
s = s[:0:0]
fmt.Println(s)
}Limiting the capacity after slicing can avoid overwriting subsequent elements of the original slice.
