Slices in Go

Slices in Go

In this tutorial, we are going to discuss slices in the Go language. A slice is a data type in Go that is a mutable, or changeable, ordered sequence of elements.

A slice is just like an array which is a container to hold elements of the same data type, but a slice can vary in size.

Internally a slice is represented by three things.

  • Pointer to the underlying array
  • The current length of the underlying array
  • Total capacity which is the maximum capacity to which the underlying array can expand.

The above internal representation is described by SliceHeader struct, which looks like this:

type SliceHeader struct {
        Pointer uintptr
        Len  int
        Cap  int
}

The Pointer field in the slice header is a pointer to the underlying array. Len is the current length of the slice, and Cap is the capacity of the slice. Similar to the array, a slice index starts from zero till length_of_slice-1. So a slice of 3 lengths and 5 capacity will look like below

Slices in Go

Syntax to define a slice is pretty similar to an array but without specifying the elements count. Hence s is a slice.

var s []int

The above code will create a slice of data type int that means it will hold elements of data type int.

But what is a zero-value of a slice? As we saw in arrays, zero value of an array is an array with all its elements being zero-value of data type it contains.

Like an array of int with size, n will have n zeroes as its elements because of zero value of int is 0. But in the case of the slice, the zero value of slice defined with the syntax above is nil.

package main

import "fmt"

func main() {
    var s []int
    fmt.Println(s == nil)
}

Output

true

Run in playground

But why nil, though, you ask because slice is just a reference to an array.

slice is a reference to an array

This may sound weird, but the slice does not contain any data. It instead stores data in an array. But then you may ask, how is that even possible when array length is fixed?

When needed to store more data, Slice creates a new array of an appropriate length behind the scene to accommodate more data.

When a slice is created by simple syntax var s []int, it is not referencing an array; hence its value is nil. Let’s now look at how it references an array.

package main

import "fmt"

func main() {
	// define empty slice
	var s []int

	fmt.Println("s == nil", s == nil)

	// create an array of int
	a := [9]int{10, 20, 30, 40, 50, 60, 70, 80, 90}

	// creates new slice
	s = a[3:7]
	fmt.Println("s == nil", s == nil, "and s = ", s)
}

Output

s == nil true 
s == nil false and s =  [40 50 60 70]

Run in playground

In the above example, we have defined a slice s of type int, but this slice doesn’t reference an array. Hence, it is nil, and the first Println statement will print true.

Later, we created an array a of type int and assigned s with a new slice returned from a[3:7]. a[3:7] syntax returns a slice from the array a starting from 3 index elements to 7 index elements.

In the above example, let’s change the value of the 3rd and 4th element of the array a (index 2 and 3, respectively) and check the value of the slice s.

package main

import "fmt"

func main() {
	var s []int
	a := [...]int{10, 20, 30, 40, 50, 60, 70, 80, 90}
	s = a[3:7]

	a[3] = 45
	a[4] = 55
	a[5] = 65

	fmt.Println(s)
}

Output

[45 55 65 70]

Run in playground

From the above result, we are convinced that slice indeed is just a reference to an array, and any change in that array will reflect in the slice.

Length and Capacity of a slice

As discussed in the arrays tutorial, to find the length of a data type, we use the len function. We are using the same len function for slices as well.

package main

import "fmt"

func main() {
	var s []int
	a := [...]int{10, 20, 30, 40, 50, 60, 70, 80, 90}
	s = a[3:7]

	fmt.Println("Length of s =", len(s))
}

Output

Length of s = 4

Run in playground

The capacity of a slice is the number of elements it can hold. Go provides a built-in function cap to get this capacity number.

package main

import "fmt"

func main() {
	var s []int
	a := [...]int{10, 20, 30, 40, 50, 60, 70, 80, 90}
	s = a[2:4]

	fmt.Println("Capacity of s =", cap(s))
}

Output

Capacity of s = 7

Run in playground

Notice in the above examples that

  • Length of newly created slice = (end–start)
  • Capacity of newly created slice = (length_of_array–start)

The above program returns 7, which is the capacity of the slice. Since slice references an array, it could have referenced array till the end. Since starting from index 2 in the above example, there are 7 elements in the array. Hence the capacity of the array is 7.

Does that mean we can grow a slice beyond its natural capacity? Yes, you can. We will find that out with the append function.

append function

You can append new values to the slice using the built-in append function. The signature of append function is

func append(slice []Type, elements ...Type) []Type

This means that append function takes a slice as the first argument, one/many elements as further arguments to append to the slice and returns a new slice of the same data type.

package main

import "fmt"

func main() {
	a := [...]int{10, 20, 30, 40, 50, 60, 70, 80, 90}
	s := a[2:4]
	newS := append(s, 55, 65)

	fmt.Println("s = ", s, "newS = ", newS)

	fmt.Println("length = ", len(newS), "capacity = ", cap(newS))

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

Output

s =  [30 40] newS =  [30 40 55 65] 
length =  4 capacity =  7 
a =  [10 20 30 40 55 65 70 80 90]

Run in playground

As we can see from the above result, s remains unchanged, and two new elements got copied to newS but look what happened to the array a. It got changed. append function mutated array referenced by slice s.

This isn’t very pleasant. Hence slices are no easy business. Use append only to self-assign the new slice like s = append(s, …), which is more manageable.

What will happen if I append more elements than the capacity of a slice?

package main

import "fmt"

func main() {
	a := [...]int{10, 20, 30, 40, 50, 60, 70, 80, 90}
	s := a[2:4]
	fmt.Printf("before => s=%v\n", s)
	fmt.Printf("before => a=%v\n", a)
	fmt.Printf("before => len=%d, cap=%d\n", len(s), cap(s))
	fmt.Println("&a[2] == &s[0] is", &a[2] == &s[0])

	s = append(s, 100, 110, 120, 130, 140, 150, 160)
	fmt.Printf("after => s=%v\n", s)
	fmt.Printf("after => a=%v\n", a)
	fmt.Printf("after => len=%d, cap=%d\n", len(s), cap(s))
	fmt.Println("&a[2] == &s[0] is", &a[2] == &s[0])
}

Output

before => s=[30 40] 
before => a=[10 20 30 40 50 60 70 80 90] 
before => len=2, cap=7 
&a[2] == &s[0] is true 
after => s=[30 40 100 110 120 130 140 150 160]
after => a=[10 20 30 40 50 60 70 80 90] 
after => len=9, cap=14 
&a[2] == &s[0] is false

Run in playground

So first, we created an array a of int and initialized it with a bunch of values. Then we created the slice s from array a starting from index 2 to 3.

From the first set of Print statements, we verified values of s and a. Then, we made sure that s references array a by matching the memory address of their respective elements. The length and capacity of the slice s is also convincing.

Then we appended the slice s with 7 more values. So we expect the slice s to have 9 elements, hence its length is 9, but we have no idea about its new capacity. Now we found that slice s got bigger than its initial capacity of 7 to 14 and its new length is 9. But array a remain unchanged.

This looks weird at first but somewhat amazing. Go figures out the math on its own that we are trying to push more values to the slice that its underneath array can’t hold, so it creates a new array with greater length and copies old slice values to it. Then new values from append are added to that array, and the origin array remains unchanged as no operation was done on it.

anonymous array slice

Until now, we saw a slice that references an array we defined deliberately. But almost all the time, you would go with an array that is hidden and not accessible to the public.

Similar to an array, slice can be defined in a similar fashion with an initial value. In this case, Go will create a hidden array to contain the values.

package main

import "fmt"

func main() {
	s := []int{10, 20, 30, 40, 50}

	fmt.Println("s=", s)
	fmt.Printf("Length = %d, Capacity = %d", len(s), cap(s))
}

Output

s = [10 20 30 40 50] 
Length = 5, Capacity = 5

Run in playground

It’s pretty obvious that the capacity of this slice is 5 because the array is created by Go, and Go preferred creating an array of length 5 as we are creating a slice of 5 elements. But what will happen when we append two more elements.

package main

import "fmt"

func main() {
	s := []int{10, 20, 30, 40, 50}
	s = append(s, 60, 70, 80)

	fmt.Println("s = ", s)
	fmt.Printf("Length = %d, Capacity = %d", len(s), cap(s))
}

Output

s =  [10 20 30 40 50 60 70 80] 
Length = 8, Capacity = 10

Run in playground

So, Go created an array of 10 length because when we are pushing 3 new elements to the slice, the original array of length 5 was not enough to hold 8 elements. No new array will be created if we append new elements to the slice unless the slice exceeds the length of 10.

Create Slice using the make function

make is a built-in function provided by go language that can also be used to create a slice. Below is the signature of the make function

func make([]{type}, length, capacity int) []{type}

Capacity is an optional parameter while creating a slice using the make function. When capacity is omitted, the capacity of the slice is equal length specified for the slice.

When using the make function, behind the scenes go allocates an array equal to the capacity. All the elements of the allocated array are initialized with the default zero value of the type. Let’s see a program illustrating this point.

package main

import "fmt"

func main() {
	numbers := make([]int, 5, 10)
	fmt.Printf("numbers=%v\n", numbers)
	fmt.Printf("length=%d\n", len(numbers))
	fmt.Printf("capacity=%d\n", cap(numbers))

	//With capacity ommited
	numbers = make([]int, 5)
	fmt.Println("\nCapacity Ommited")
	fmt.Printf("numbers=%v\n", numbers)
	fmt.Printf("length=%d\n", len(numbers))
	fmt.Printf("capacity=%d\n", cap(numbers))
}
numbers=[0 0 0 0 0]
length=5
capacity=10

Capacity Ommited
numbers=[0 0 0 0 0]
length=5
capacity=5

Run in playground

Create Slice using the new function

new is a built-in function provided by go that can also be used to create a slice. It is not a very popular way of creating a slice as make is much more flexible in terms of functionalities. 

It is not generally used, and also, using a new function returns a pointer to nil slice. Let’s see an example. In the below example, we use the dereferencing operator â€˜*’ as the new function returns a pointer to the nil slice.

package main

import "fmt"

func main() {
    numbers := new([]int)
    fmt.Printf("numbers=%v\n", *numbers)
    fmt.Printf("length=%d\n", len(*numbers))
    fmt.Printf("capacity=%d\n", cap(*numbers))
}

Output

numbers=[]
length=0
capacity=0
Copy a slice

Go builtin package provides a copy function that can be used to copy a slice. Below is the signature of this function. It takes in two slices dest and src, and copies data from src to dest. It returns the number of elements copied.

func copy(dest, src []Type) int

There are 2 cases to be considered while using the copy function:

  • If the length of src is greater than the length of dest, then the number of elements copied is the length of dest.
  • If the length of dest is greater than the length of src, then the number of elements copied is the length of src.

Basically, the number of elements copied is the minimum length of (src, dest). 

Also, note that once the copy is done, any change in dest will not reflect in src and vice versa. Let’s see an example of it.

package main

import "fmt"

func main() {
	src := []int{1, 2, 3, 4, 5}
	dest := make([]int, 5)

	numberOfElementsCopied := copy(dest, src)
	fmt.Printf("Number Of Elements Copied: %d\n", numberOfElementsCopied)
	fmt.Printf("dest: %v\n", dest)
	fmt.Printf("src: %v\n", src)

	//After changing numbers2
	dest[0] = 10
	fmt.Println("\nAfter changing dest")
	fmt.Printf("dest: %v\n", dest)
	fmt.Printf("src: %v\n", src)
}

Output

Number Of Elements Copied: 5
dest: [1 2 3 4 5]
src: [1 2 3 4 5]

After changing dest
dest: [10 2 3 4 5]
src: [1 2 3 4 5]

Run in playground

Slices in Go
Scroll to top