[A Tour of Go Study Notes] 05 Arrays and Slices
Understanding arrays and slices in the Go language using the official tutorial
Let’s use the official tutorial, A Tour of Go
, to get familiar with basic Golang usage.
This piece introduces the concepts of arrays and slices.
[n]T
represents an array with n
elements of type T
.
Therefore, the value types within an array must be consistent, and the length remains fixed.
var a [10]int // a is an array of 10 pure integers
package main
import "fmt"
func main() {
var a [2]string
a[0] = "Hello"
a[1] = "World"
fmt.Println(a[0], a[1])
fmt.Println(a)
primes := [6]int{2, 3, 5, 7, 11, 13}
fmt.Println(primes)
}
/*
>>> Hello World
>>> [Hello World]
>>> [2 3 5 7 11 13]
*/
It seems using arrays can be quite challenging. Who would know the length from the start?
Don’t worry, Go provides a more convenient way to handle arrays.
Slices
Arrays have a fixed size, whereas slices offer dynamic sizing and are generally more frequently used than arrays.
[]T
represents a slice with elements of type T
, accessed via a lower and upper bound to obtain a subset of array elements.
Retrieve elements including low
but excluding high
.
a[low: high]
The following example creates a slice a
with elements from 1 to 3.
package main
import "fmt"
func main() {
primes := [6]int{2, 3, 5, 7, 11, 13}
var s []int = primes[1:4]
fmt.Println(s)
}
/*
>>> [3 5 7]
*/
A slice itself does not store any elements but rather references the underlying array.
Therefore, modifying the content of a slice affects other slices using the same underlying array!
package main
import "fmt"
func main() {
names := [4]string{
"John",
"Paul",
"George",
"Ringo",
}
fmt.Println(names)
a := names[0:2]
b := names[1:3]
fmt.Println(a, b)
b[0] = "XXX"
fmt.Println(a, b)
fmt.Println(names)
}
/*
>>> [John Paul George Ringo]
>>> [John Paul] [Paul George]
>>> [John XXX] [XXX George]
>>> [John XXX George Ringo]
*/
Slices can contain any type
, even other slices.
package main
import (
"fmt"
"strings"
)
func main() {
// A tic-tac-toe game template
board := [][]string{
[]string{"_", "_", "_"},
[]string{"_", "_", "_"},
[]string{"_", "_", "_"},
}
// Two players taking turns to place O and X
board[0][0] = "X"
board[2][2] = "O"
board[1][2] = "X"
board[1][0] = "O"
board[0][2] = "X"
for i := 0; i < len(board); i++ {
fmt.Printf("%s\n", strings.Join(board[i], " "))
}
}
/*
>>> X _ X
>>> O _ X
>>> _ _ O
*/
Slice Declaration
Slice declaration resembles an array without a length.
This is an array declaration.
[3]bool{true, true, false}
This is a slice expression. It creates an array similar to the one above and then constructs a slice that refers to this array.
[]bool{true, true, false}
package main
import "fmt"
func main() {
q := []int{2, 3, 5, 7, 11, 13}
fmt.Println(q)
r := []bool{true, false, true, true, false, true}
fmt.Println(r)
s := []struct {
i int
b bool
}{
{2, true},
{3, false},
{5, true},
{7, true},
{11, false},
{13, true},
}
fmt.Println(s)
}
/*
>>> [2 3 5 7 11 13]
>>> [true false true true false true]
>>> [{2 true} {3 false} {5 true} {7 true} {11 false} {13 true}]
*/
Default Value of Slices
The default value of a slice is nil
.
A nil
slice has a length and capacity of 0 and does not reference an underlying array.
package main
import "fmt"
func main() {
var s []int
fmt.Println(s, len(s), cap(s))
if s == nil {
fmt.Println("nil!")
}
}
/*
>>> [] 0 0
>>> nil!
*/
Default Behavior of Slices
Slices by default ignore the upper and lower bounds.
Or rather, the default lower bound is 0, and the upper bound is the length of the slice.
Assuming an array:
var a [10]int
The following slice contents are equivalent:
a[0:10]
a[:10]
a[0:]
a[:]
In the example below, since a slice modifies the referenced array, the contents of s
itself are altered.
package main
import "fmt"
func main() {
s := []int{2, 3, 5, 7, 11, 13}
s = s[1:4]
fmt.Println(s)
s = s[:2]
fmt.Println(s)
s = s[1:]
fmt.Println(s)
}
/*
>>> [3 5 7]
>>> [3 5]
>>> [5]
*/
Slice Length and Capacity
- Length: the number of elements it contains
- Capacity: the number of elements in the underlying array starting from the first element of the slice
You can obtain the length using len(s)
and the capacity using cap(s)
.
Slices altered by slicing can be expanded by reslicing.
Given enough capacity from the original array length:
package main
import "fmt"
func main() {
s := []int{2, 3, 5, 7, 11, 13}
printSlice(s)
// Slice the slice to give it zero length.
s = s[:0]
printSlice(s)
// Extend its length.
s = s[:4]
printSlice(s)
// Drop its first two values.
s = s[2:]
printSlice(s)
}
func printSlice(s []int) {
fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}
/*
>>> len=6 cap=6 [2 3 5 7 11 13]
>>> len=0 cap=6 []
>>> len=4 cap=6 [2 3 5 7]
>>> len=2 cap=4 [5 7]
*/
However, if it exceeds the capacity of the underlying array, it will cause a panic
.
For instance, if the initial slice length is 6, and we attempt to expand the slice’s capacity to 9:
s := []int{2, 3, 5, 7, 11, 13}
s = s[:9]
printSlice(s)
/*
>>> panic: runtime error: slice bounds out of range [:9] with capacity 6
*/
Creating Slices using make
Slices can be created using make
, which is another method to create dynamic arrays.
The make
function allocates an array of specified length with all elements initialized to zero and returns a reference to a slice referring to this array.
If a capacity is desired, the third argument needs to be passed to make
.
package main
import "fmt"
func main() {
a := make([]int, 5)
printSlice("a", a)
b := make([]int, 0, 5)
printSlice("b", b)
c := b[:2]
printSlice("c", c)
d := c[2:5]
printSlice("d", d)
}
func printSlice(s string, x []int) {
fmt.Printf("%s len=%d cap=%d %v\n",
s, len(x), cap(x), x)
}
/*
>>> a len=5 cap=5 [0 0 0 0 0]
>>> b len=0 cap=5 []
>>> c len=2 cap=5 [0 0]
>>> d len=3 cap=3 [0 0 0]
*/
Adding Elements to a Slice
Appending new elements to a slice is a common operation.
In Golang, we use Append
for this purpose, a built-in function you can reference here: Official Documentation
The append
expression’s first argument s
is a slice of type T
.
The rest of the values of type T
will be added to the end of the existing slice.
func append(s []T, ...T) []T
The resulting slice will contain all the elements from the original slice along with the new added elements.
If the underlying array of s
is too small to fit all the given values, s
will be assigned a larger array, and the returned slice will refer to this new allocated array.
To learn more about slice internals, you can read this article: Go Slices: usage and internals
package main
import "fmt"
func main() {
var s []int
printSlice(s)
// Appending an empty slice
s = append(s, 0)
printSlice(s)
// Slice grows as needed
s = append(s, 1)
printSlice(s)
// Multiple elements can be added at once
s = append(s, 2, 3, 4)
printSlice(s)
}
/*
>>> len=0 cap=0 []
>>> len=1 cap=1 [0]
>>> len=2 cap=2 [0 1]
>>> len=5 cap=6 [0 1 2 3 4]
*/