Working with slice in Go.

Slice in Go can be considered as dynamic version of array.

[]T is a slice of type T. Length and capacity of slice can be extended at run-time. This makes slice very flexible and widely used compare to array.

A new slice can be created by using make([]T, length, capacity), slice literals or slicing from an existing array/slice:

// using make
s0 := make([]int, 2)
s1 := make([]int, 0, 5)
s2 := make([]int, 2, 5)

// slice literals
s3 := []float64{}
s4 := []float64{1.0, 2.0, 3.0}

// slicing from existing array
arr := [5]int{1, 2, 3, 4, 5}
s5 := arr[0:1]
s6 := arr[:]
s7 := s6[2:3]

Playground

Length is number of elements of the slice and capacity is number elements the underlying array can hold (see explain about underlying array in advanced section). And they can be calculated by using 2 built-in functions len and cap

s := make([]int, 0, 5)
fmt.Printf("len: %d, cap: %d", len(s), cap(s)) // len: 0, cap: 5

A new element can be added into a slice using append function:

s := make([]int, 0, 5)
s = append(s, 1)
s = append(s, 2, 3, 4, 5)
fmt.Printf("len: %d, cap: %d, values: %v", len(s), cap(s), s) // len: 5, cap: 5, values: [1 2 3 4 5]

Slice element can be accessed via index and range statement can be used to range over the slice:

a := []int{1, 2, 3, 4, 5}

// get value by index
fmt.Println("a[1]:", a[1])

// modify value by index
a[2] = 4
fmt.Println("a:", a)

// range over the slice
for i, v := range a {
fmt.Printf("index: %d, value: %d\n", i, v)
}

Playground

A slice can be copied to another one by using copy or append:

a := []int{1, 2, 3, 4, 5}

// using copy
b := make([]int, len(a))
copy(b, a)

// using append
c := append([]int(nil), a...)

// or another way with append
d := append(a[:0:0], a...)

Playground

append can be used to cut/delete 1 or more elements in the middle of a slice:

a := []int{1, 2, 3, 4, 5}
fmt.Println("a: ", a)

a = append(a[:1], a[2:]...) // cut the 2rd element
fmt.Println("a: ", a)

a = append(a[:1], a[3:]...) // cut the 2rd & 3rd element
fmt.Println("a: ", a)

Playground

For more tricks of using slice, see: https://github.com/golang/go/wiki/SliceTricks

Slice can be considered as reference type. This means modifying its value inside a function will change the values of the original slice:

func main() {
a := []int{1, 2, 3, 4, 5}
changeValue(a, 0, 2)

fmt.Println("a: ", a)
}

func changeValue(a []int, i int, v int) {
a[i] = v
}

Playground

Capacity of slice will be double if its length reaches its current capacity: (more complex strategy if length of slice is greater than 1024, see slice.go)

a := make([]int, 0, 2)
a = append(a, 1, 2) // len: 2, cap: 2
a = append(a, 3) // len: 3, cap: 4
a = append(a, 4, 5) // len: 5, cap: 8
a = append(a, 6) // len: 6, cap: 8

Playground

Advanced #

A slice is actually a composite type named SliceHeader which is composed of 3 words: Data, Len and Cap. Data is a pointer that point to the first element of the underlying array that the slice can reach (not necessary the first element of the array). Len is number elements of the slice and Cap is length of the underlying array:

type SliceHeader struct {
Data uintptr
Len int
Cap int
}

A slice can be reverse back to SliceHeader by using reflect and unsafe package:

a := make([]int, 0, 5)
a = append(a, 1, 2)

h := (*reflect.SliceHeader)(unsafe.Pointer(&a))

fmt.Printf("Data: %d, Len: %d, Cap: %d", h.Data, h.Len, h.Cap) // Data: 824634388208, Len: 2, Cap: 5

Playground

We can also reverse the pointer that point to the underlying array to an array using unsafe and type casting:

a := make([]int, 0, 5)
a = append(a, 1, 2)

h := (*reflect.SliceHeader)(unsafe.Pointer(&a))
arr := (*[5]int)(unsafe.Pointer(h.Data))

fmt.Printf("%16s: %v\n", "slice", a) // [1 2]
fmt.Printf("%16s: %v\n", "underlying array", *arr) // [1 2 0 0 0]

Playground

When a slice is passed into a function, a copied version of the SliceHeader is created, but the data is still the same:

func main() {
a := make([]int, 0, 5)
a = append(a, 1, 2)

h := (*reflect.SliceHeader)(unsafe.Pointer(&a))

// Address: 824633770032, Data: 824634212400, Len: 2, Cap: 5
fmt.Printf("Main - Address: %d, Data: %d, Len: %d, Cap: %d\n", unsafe.Pointer(&a), h.Data, h.Len, h.Cap)

printSliceInfo(a)
}

func printSliceInfo(a []int) {
h := (*reflect.SliceHeader)(unsafe.Pointer(&a))
// Address: 824633770056, Data: 824634212400, Len: 2, Cap: 5
fmt.Printf("Func - Address: %d, Data: %d, Len: %d, Cap: %d\n", unsafe.Pointer(&a), h.Data, h.Len, h.Cap)
}

Playground

append statement always return a new slice header (as of Go 1.13):

a := make([]int, 0, 5)
a1 := append(a, 1, 2) // Address: 824633770032, Data: 824634212400, Len: 2, Cap: 5
a2 := append(a, 1, 2) // Address: 824634425392, Data: 824634441776, Len: 2, Cap: 5

Playground

One of the reason make always return a new SliceHeader is because the underlying array will be re-allocated to a completely new one if length of slice reach its capacity:

a := make([]int, 0, 5)            // capacity = 5
a1 := append(a, 1, 2, 3, 4, 5, 6) // exceed capacity, new underlying array will be allocated.

Playground

Questions #

What the below program print and why?

package main

import "fmt"

func main() {
arr := [5]int{1, 2, 3, 4, 5}
s := arr[0:2]
s = append(s, 6)

fmt.Println(arr)
}

What the below program print and why?

package main

import "fmt"

func main() {
arr := [5]int{1, 2, 3, 4, 5}
s := arr[0:2]
_ = append(s, 6)

fmt.Println(arr)
fmt.Println(s)
}

What the below program print and why?

package main

import "fmt"

func main() {
arr := [5]int{1, 2, 3, 4, 5}
s := arr[0:2]
s1 := arr[1:3]

s = append(s, 6)

fmt.Println(s)
fmt.Println(s1)
}