Byte Ebi's Logo

Byte Ebi 🍀

A Bit everyday A Byte every week

[A Tour of Go Study Notes] 05 Arrays and Slices

Understanding arrays and slices in the Go language using the official tutorial

Ray

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]
*/

Recent Posts

Categories

Tags