Slices

A slice is Go's everyday list: a sequence of elements of one type that can grow and shrink. Slices are used everywhere — function parameters, results from the standard library, lines read from a file. This lesson covers creating slices, adding elements with append, slicing with s[low:high], copying, and nil versus empty slices. The next lesson looks at how they work underneath.

Creating slices

GO
package main

import "fmt"

func main() {
	langs := []string{"Go", "Rust", "Python"} // a slice literal: no length in []
	fmt.Println(langs, len(langs))

	var ids []int // nil slice: length 0, ready to append to
	fmt.Println(ids, len(ids), ids == nil)

	scores := make([]int, 3) // three zeros
	scores[1] = 88
	fmt.Println(scores)

	buffer := make([]byte, 0, 1024) // length 0, room for 1024
	fmt.Println(len(buffer), cap(buffer))
}
Output
[Go Rust Python] 3
[] 0 true
[0 88 0]
0 1024

The type []string has no length in the brackets — that's what makes it a slice rather than an array. make([]T, length, capacity) creates a slice of a given length, filled with zero values; the optional capacity reserves extra room for growth (next lesson).

Adding elements with append

append adds one or more elements to the end and returns the updated slice, which you must assign back:

GO
package main

import "fmt"

func main() {
	var cart []string
	cart = append(cart, "milk")
	cart = append(cart, "bread", "eggs")
	fmt.Println(cart, len(cart))

	more := []string{"tea", "jam"}
	cart = append(cart, more...) // ... spreads a slice into separate arguments
	fmt.Println(cart)
}
Output
[milk bread eggs] 3
[milk bread eggs tea jam]

Forgetting the assignment — writing just append(cart, "milk") — is a compile error, because the result is unused. Appending to a nil slice works perfectly, so there's no need to create an empty one first.

Slicing

s[low:high] makes a slice of the elements from index low up to, but not including, high. Either index can be left out:

GO
package main

import "fmt"

func main() {
	letters := []string{"a", "b", "c", "d", "e", "f"}
	fmt.Println(letters[1:4]) // indexes 1, 2, 3
	fmt.Println(letters[:2])  // the first two
	fmt.Println(letters[4:])  // from index 4 to the end
	fmt.Println(letters[:])   // everything
	fmt.Println(letters[len(letters)-1])
}
Output
[b c d]
[a b]
[e f]
[a b c d e f]
f

Go has no negative indexes: for the last element write s[len(s)-1]. The number of elements in s[low:high] is high - low.

Slices share their elements

Slicing doesn't copy. The new slice is a window onto the same underlying elements, so a change through one is visible through the other:

GO
package main

import "fmt"

func main() {
	week := []string{"Mon", "Tue", "Wed", "Thu", "Fri"}
	midweek := week[1:4]
	midweek[0] = "TUESDAY"
	fmt.Println(week)
	fmt.Println(midweek)
}
Output
[Mon TUESDAY Wed Thu Fri]
[TUESDAY Wed Thu]

The same is true when you pass a slice to a function: the function gets a small header pointing at the same elements, so it can change them (but appending inside the function doesn't change the caller's length — lesson 26).

GO
package main

import "fmt"

func applyDiscount(prices []float64, pct float64) {
	for i := range prices {
		prices[i] *= 1 - pct/100
	}
}

func main() {
	prices := []float64{20, 50, 80}
	applyDiscount(prices, 10)
	fmt.Println(prices)
}
Output
[18 45 72]

Copying

When you need an independent copy, use slices.Clone, or the built-in copy(dst, src), which copies as many elements as fit in dst and returns the number copied:

GO
package main

import (
	"fmt"
	"slices"
)

func main() {
	original := []int{1, 2, 3, 4}

	clone := slices.Clone(original)
	clone[0] = 99

	firstTwo := make([]int, 2)
	n := copy(firstTwo, original)

	fmt.Println(original, clone, firstTwo, n)
}
Output
[1 2 3 4] [99 2 3 4] [1 2] 2

Removing and inserting

The slices package (lesson 27) has Delete and Insert, which handle the shifting for you:

GO
package main

import (
	"fmt"
	"slices"
)

func main() {
	queue := []string{"ana", "ben", "cleo", "dev"}
	queue = slices.Delete(queue, 1, 2) // remove index 1
	fmt.Println(queue)

	queue = slices.Insert(queue, 0, "zoe") // insert at the front
	fmt.Println(queue)

	queue = queue[1:] // drop the first element by re-slicing
	fmt.Println(queue)
}
Output
[ana cleo dev]
[zoe ana cleo dev]
[ana cleo dev]

Nil and empty slices

A nil slice (var s []int) and an empty slice ([]int{}) both have length 0 and behave the same for len, range and append. Test for "no elements" with len(s) == 0, not s == nil. The difference shows up mainly when encoding JSON, where a nil slice becomes null and an empty one becomes []:

GO
package main

import (
	"encoding/json"
	"fmt"
)

func main() {
	var nilSlice []string
	emptySlice := []string{}
	fmt.Println(len(nilSlice), len(emptySlice), nilSlice == nil, emptySlice == nil)

	a, _ := json.Marshal(nilSlice)
	b, _ := json.Marshal(emptySlice)
	fmt.Println(string(a), string(b))
}
Output
0 0 true false
null []

Slices of slices

Each inner slice can have a different length, which makes them handy for jagged data such as lines of words:

GO
package main

import "fmt"

func main() {
	triangle := [][]int{}
	for row := range 4 {
		line := make([]int, row+1)
		line[0], line[row] = 1, 1
		for i := 1; i < row; i++ {
			line[i] = triangle[row-1][i-1] + triangle[row-1][i]
		}
		triangle = append(triangle, line)
	}
	for _, line := range triangle {
		fmt.Println(line)
	}
}
Output
[1]
[1 1]
[1 2 1]
[1 3 3 1]

Try it yourself

Keep only the even numbers. This uses a common trick: nums[:0] is an empty slice that reuses nums's storage, so no new memory is needed.

Try it yourself · main.go
Edit the code, copy it into main.go in a new folder and run it with go run main.go (Go 1.27 or later). How to install Go
Output
[8 12 6 10] 4
Next: how slices work under the hood — length, capacity and growth.