Mastering Slices For Dynamic Data Structures: A Deep Dive Into Slice Manipulation And Memory Efficiency

Overview

This chapter takes an in-depth look at slices in Go, a powerful tool for working with dynamic data structures. You’ll start by understanding the basics of slice creation, initialization, and iteration. Then, you’ll explore advanced techniques for growing, shrinking, and manipulating slices without unnecessary memory allocation. Key topics include the append function, slice internals, and memory management techniques like copying and deleting from slices. Additionally, you will learn about Go’s slices package, which provides enhanced tools for sorting, searching, and managing slices efficiently. You’ll also explore the iterator functions introduced in Go 1.23, enabling powerful slice transformations and batch processing. By mastering slices, you’ll be able to work more effectively with Go’s dynamic data structures.

What Are Slices

  • Similar to Arrays
  • Fixed type
  • Dynamically sized
  • Flexible

package main

import "fmt"

func main() {
	// create an array of names
	namesArray := [4]string{"John", "Paul", "George", "Ringo"}
	// create a slice of names
	namesSlice := []string{"John", "Paul", "George", "Ringo"}

	fmt.Println(namesArray)
	fmt.Println(namesSlice)
}

Iterating Over Slices

Slices can be iterated over in the same ways that arrays can be.


package main

import "fmt"

func main() {
	names := []string{"John", "Paul", "George", "Ringo"}

	for i, n := range names {
		fmt.Printf("%d - %s\n", i, n)
	}
}

Why Slices?

  • Change a slice without an allocation
  • Operate on subsections of slices easily
  • Dynamically grow the size of a slice

How can you tell the difference between a slice and an array?

Slices don’t have a length in the declaration:

var array [5]string // array
var slice []string // slice

Slice Internals

Think of a slice as having three members:

  • Length
  • Capacity
  • Pointer to the underlying array

type slice struct {
	Length   int
	Capacity int
	Array    [10]array
}

NOTE: This is not the actual definition, but a way you can think about a slice header.

Appending To Slices

The append keyword allows us to add elements to a slice.


package main

import "fmt"

func main() {
	names := []string{}
	names = append(names, "John")

	// Append multiple items at once
	names = append(names, "Sally", "George")

	// Append an entire slice to another slice
	moreNames := []string{"Bill", "Ginger", "Wilma"}
	names = append(names, moreNames...)

	fmt.Println(names)
}

Should the slice not have enough space, Go will automatically reallocate the slice to have more capacity.

The Ellipsis Operator (...)

The ellipsis operator (...) has multiple important uses in Go. Understanding when and how to use it is essential for working with slices effectively.



// Example variadic function that accepts any number of integers
func sum(numbers ...int) int {
	total := 0
	for _, n := range numbers {
		total += n
	}
	return total
}

// Another variadic function
func printNames(prefix string, names ...string) {
	fmt.Printf("%s: ", prefix)
	for i, name := range names {
		if i > 0 {
			fmt.Print(", ")
		}
		fmt.Print(name)
	}
	fmt.Println()
}

func main() {
	// The ellipsis operator (...) has five main uses in Go:

	// Use 1: Variadic function parameters
	// Allows a function to accept a variable number of arguments
	fmt.Println("--- Use 1: Variadic Function Parameters ---")
	fmt.Println("sum(1, 2, 3):", sum(1, 2, 3))
	fmt.Println("sum(10, 20, 30, 40, 50):", sum(10, 20, 30, 40, 50))

	// Use 2: Unpacking slices into variadic functions
	// The ... operator "unpacks" a slice into individual arguments
	fmt.Println("\n--- Use 2: Unpacking Slices ---")
	numbers := []int{5, 10, 15, 20}
	fmt.Printf("numbers slice: %v\n", numbers)
	fmt.Printf("sum(numbers...): %d\n", sum(numbers...))

	// Without ..., this would be a compile error:
	// sum(numbers) // ERROR: cannot use numbers (type []int) as type int

	// Use 3: Unpacking slices with append
	fmt.Println("\n--- Use 3: Append with Slice Unpacking ---")
	fruits := []string{"apple", "banana"}
	moreFruits := []string{"cherry", "date", "elderberry"}

	// Append individual elements
	fruits = append(fruits, "fig")
	fmt.Printf("After append single: %v\n", fruits)

	// Append entire slice - requires ... to unpack
	fruits = append(fruits, moreFruits...)
	fmt.Printf("After append slice:  %v\n", fruits)

	// Use 4: Array literals with inferred length
	fmt.Println("\n--- Use 4: Array Literals (inferred length) ---")
	// The compiler counts the elements and sets the array size
	inferredArray := [...]string{"red", "green", "blue"}
	fmt.Printf("Array: %v (type: %T, len: %d)\n", inferredArray, inferredArray, len(inferredArray))

	// This is equivalent to:
	explicitArray := [3]string{"red", "green", "blue"}
	fmt.Printf("Array: %v (type: %T, len: %d)\n", explicitArray, explicitArray, len(explicitArray))

	// Use 5: Combining variadic functions
	fmt.Println("\n--- Use 5: Practical Example ---")
	allNames := []string{"Alice", "Bob", "Charlie"}
	printNames("Team members", allNames...)

	// You can also mix literal values with slice unpacking in some contexts
	moreNames := []string{"David", "Eve"}
	combined := append(allNames, moreNames...)
	printNames("All members", combined...)

	// Common gotcha: Can't use ... on left side of assignment
	fmt.Println("\n--- Common Gotcha ---")
	fmt.Println("The ... operator ONLY works on the right side (as an argument)")
	fmt.Println("You CANNOT use it to destructure/unpack on the left side like:")
	fmt.Println("  a, b, c... := []int{1, 2, 3, 4, 5}  // This is INVALID")
}

Key uses:

  • Variadic function parameters (accepting variable number of arguments)
  • Unpacking slices into individual arguments
  • Using with append to concatenate slices
  • Array literals with compiler-inferred length

Deleting From Slices

There are two ways to delete an item from a slice. Either manually, or using the slice.Delete function from the slices package.

Manually deleting from a slice requires you to calculate the specific item you wish to remove:


package main

import "fmt"

func main() {
	a := []string{"zero", "one", "two", "three", "four", "five"}
	// remove 4 items, starting with the index of 1 (second item in slice)
	// append(a[:i], a[i+(number of items to remove):]...) -> i = 1, i+4=5
	a = append(a[:1], a[5:]...)
	fmt.Println(a)
}

Using the slices.Delete function, you can simply provide the slice, the starting index, and the number of elements to delete:


package main

import (
	"fmt"

	"slices"
)

func main() {
	a := []string{"zero", "one", "two", "three", "four", "five"}
	// slices.Delete(s, i, j) -> s = slice having items deleted from, i = start index, j = end index
	a = slices.Delete(a, 1, 4)
	fmt.Println(a)
}

The slices package is part of the standard library since Go 1.21

Growing A Slice


names := []string{}
fmt.Println("len:", len(names)) // 0
fmt.Println("cap:", cap(names)) // 0

names = append(names, "John")
fmt.Println("len:", len(names)) // 1
fmt.Println("cap:", cap(names)) // 1

names = append(names, "Paul")
fmt.Println("len:", len(names)) // 2
fmt.Println("cap:", cap(names)) // 2

names = append(names, "George")
fmt.Println("len:", len(names)) // 3
fmt.Println("cap:", cap(names)) // 4

names = append(names, "Ringo")
fmt.Println("len:", len(names)) // 4
fmt.Println("cap:", cap(names)) // 4

names = append(names, "Stu")
fmt.Println("len:", len(names)) // 5
fmt.Println("cap:", cap(names)) // 8

Len And Cap


fmt.Println("len:", len(names)) // 5
fmt.Println("cap:", cap(names)) // 8

The len keyword tells us how many elements the slice actually has.

The cap keyword tells us the capacity of the slice, or how many elements it can have.

What Happens When A Slice Grows

Given the following slice:

┌────────────────────────────────────────────────┐
│                  Slice Header                  │
├────────────────────────────────────────────────┤
│ Len -> 4                                       │
│                                                │
│ Cap -> 4                                       │
│                          ┌─────────────────┐   │
│ Arr -> Array Pointer ->  │Underlying Array │   │
│                          │                 │   │
│                          │┌─┬─┬─┬─┐        │   │
│                          ││A│B│C│D│        │   │
│                          │└─┴─┴─┴─┘        │   │
│                          └─────────────────┘   │
└────────────────────────────────────────────────┘

If we append the values E, F, and G, it will force the slice to expand, as it currently has no capacity for the new values.

It will create a new underlying array, copy the original values into the new one, and add the new values as well.

┌────────────────────────────────────────────────┐
│          append(slice, "E", "F", "G")          │
├────────────────────────────────────────────────┤
│                                                │
│ ┌─────────────────┐        ┌─────────────────┐ │
│ │Original Array   │        │New Array        │ │
│ │                 │        │                 │ │
│ │┌─┬─┬─┬─┐        │───────>│┌─┬─┬─┬─┬─┬─┬─┬─┐│ │
│ ││A│B│C│D│        │        ││A│B│C│D│E│F│G│ ││ │
│ │└─┴─┴─┴─┘        │        │└─┴─┴─┴─┴─┴─┴─┴─┘│ │
│ └─────────────────┘        └─────────────────┘ │
│                                                │
└────────────────────────────────────────────────┘

This is what the new slice will look like:

┌────────────────────────────────────────────────┐
│                  Slice Header                  │
├────────────────────────────────────────────────┤
│ Len -> 7                                       │
│                                                │
│ Cap -> 8                                       │
│                          ┌─────────────────┐   │
│ Arr -> Array Pointer ->  │Underlying Array │   │
│                          │                 │   │
│                          │┌─┬─┬─┬─┬─┬─┬─┬─┐│   │
│                          ││A│B│C│D│E│F│G│ ││   │
│                          │└─┴─┴─┴─┴─┴─┴─┴─┘│   │
│                          └─────────────────┘   │
└────────────────────────────────────────────────┘

If the original underlying array is not reference by any other part of the program, it will be marked for garbage collection.

Making A Slice

Slices can also be created using the make keyword.


package main

import "fmt"

func main() {
	a := []string{}
	b := make([]string, 0)
	var c []string

	fmt.Println(a)
	fmt.Println(b)
	fmt.Println(c)
}

All of the slices are functionally equivalent, but the first version is easier to read (and write).

Make With Length And Capacity

The make keyword allows us to define the starting “length” of the slice, and optionally, the starting “capacity” of the slice.


package main

import "fmt"

func main() {
	a := make([]int, 1, 3)

	fmt.Println(a)      // [0]
	fmt.Println(len(a)) // 1
	fmt.Println(cap(a)) // 3
}

It’s important to remember that even though you allocated extra capacity, you can’t access that capacity until you assign a value to it

Make And Append

Be careful when using both make and append, as you may inadvertantly create zero values in your slice.


package main

import "fmt"

func main() {
	a := make([]string, 2)
	a = append(a, "foo", "bar")
	fmt.Printf("%q", a)
}

Two Dimensional Slices

Go’s slices are one-dimensional. To create an equivalent of a 2D slice, it is necessary to define an slice-of-slices.

Because slices are variable-length, it is possible to have each inner slice be a different length.


type Modules [][]string

course := Modules{
	[]string{"Chapter One: Syntax"},
	[]string{"Chapter Two: Arrays and Slices"},
	[]string{"Chapter Three: Maps", "Chapter Four: Packages"},
}

Slice Subsets

Subsets of a slice (or a slice of a slice) allow us to work with just section of a slice.


package main

import (
	"fmt"
)

// slice[starting_index : (starting_index + length)]

func main() {
	names := []string{"John", "Paul", "George", "Ringo"}

	fmt.Println(names) // [John Paul George Ringo]

	// Get 2 elements starting with the second element (index 1)
	fmt.Println(names[1:3]) // [Paul George] - names[1:1+2]

	// functionally equivalent
	fmt.Println(names[2:len(names)]) // [George Ringo]
	fmt.Println(names[2:])           // [George Ringo]

	// functionally equivalent
	fmt.Println(names[0:2]) // [John Paul]
	fmt.Println(names[:2])  // [John Paul]
}

Note: You can not use negative numbers in slices. The following is invalid syntax:

names[:-2]

This will result in an error:

invalid slice index -2 (index must be non-negative)

Mutating Slice Subsets

It is important to remember that when grabbing a subset of a slice, you are just getting a “window” into that slice.

If you mutate the subset, you mutate the original slice.


// slice[starting_index : (starting_index + length)]

func main() {
	names := []string{"John", "Paul", "George", "Ringo"}

	fmt.Println(names) // [John Paul George Ringo]

	// Get the first three elements of the `names` slice
	guitars := names[:3]

	fmt.Println(guitars) // [John Paul George]

	for i, g := range guitars {
		guitars[i] = strings.ToUpper(g)
	}

	// Print out our original slice of names
	fmt.Println("names:  ", names)

	// Print out the guitars slice
	fmt.Println("guitars:", guitars)
}

Sub-slice Sharing After Append

A common gotcha occurs when using append() on a slice that has spare capacity. If the capacity is not exceeded, the new slice shares the same underlying array as the original, meaning modifications can affect both slices.


func main() {
	// Create a slice with length 2 but capacity 4
	// This means there's room to append without reallocation
	a := make([]string, 2, 4)
	a[0] = "apple"
	a[1] = "banana"

	fmt.Printf("Initial slice a: %v (len=%d, cap=%d)\n", a, len(a), cap(a))

	// Create a new slice by appending to 'a'
	// Since capacity is not exceeded, 'b' shares the same underlying array
	b := append(a, "cherry")
	fmt.Printf("After append, slice b: %v (len=%d, cap=%d)\n", b, len(b), cap(b))

	// Modifying 'b' at index 0 affects 'a' because they share the underlying array
	b[0] = "MODIFIED"

	fmt.Println("\nAfter modifying b[0]:")
	fmt.Printf("slice a: %v\n", a) // a is affected!
	fmt.Printf("slice b: %v\n", b)

	fmt.Println("\n--- Example 2: Exceeding capacity ---")

	// Now let's exceed the capacity
	c := make([]string, 2, 2)
	c[0] = "dog"
	c[1] = "cat"

	fmt.Printf("\nInitial slice c: %v (len=%d, cap=%d)\n", c, len(c), cap(c))

	// Appending here exceeds capacity, so a new array is allocated
	d := append(c, "bird")
	fmt.Printf("After append, slice d: %v (len=%d, cap=%d)\n", d, len(d), cap(d))

	// Modifying 'd' does NOT affect 'c' because they use different arrays
	d[0] = "MODIFIED"

	fmt.Println("\nAfter modifying d[0]:")
	fmt.Printf("slice c: %v\n", c) // c is NOT affected
	fmt.Printf("slice d: %v\n", d)
}

Key takeaway: When capacity is not exceeded, append creates a new slice header but reuses the underlying array. When capacity is exceeded, a new array is allocated and data is copied.

Copy

You can use the copy keyword to make a copy without sharing reference to the original underlying array.


package main

import "fmt"

func main() {
	original := []string{"carrot", "potato", "cucumber", "onion"}
	// create a new reference to the existing slice
	ref := original

	// initialize a variable named 'dup' to the
	// same size as the original slice
	dup := make([]string, len(original))

	// copy the values from original to dup
	// NOTE: if the slices are not the same size, it will
	// only copy what it has space for (length)
	copy(dup, original)

	// changing either `ref` or `original` will change either
	// of them as they still share the reference to the same
	// backing array
	ref[0] = "zuchinni"
	original[1] = "tomato"

	fmt.Println("original: ", original)
	fmt.Println("ref:      ", ref)
	fmt.Println("dup:      ", dup)
}

Copy Behavior With Different Sizes

The copy() function copies min(len(src), len(dst)) elements. Understanding this behavior is crucial to avoid common pitfalls.


func main() {
	// The copy() function copies min(len(src), len(dst)) elements
	// This means it only copies as many elements as will fit

	// Example 1: Copying to a zero-size slice (copies 0 elements)
	fmt.Println("--- Example 1: Copying to zero-size slice ---")
	source1 := []string{"apple", "banana", "cherry"}
	dest1 := []string{} // zero length

	n1 := copy(dest1, source1)
	fmt.Printf("Source: %v (len=%d)\n", source1, len(source1))
	fmt.Printf("Dest:   %v (len=%d)\n", dest1, len(dest1))
	fmt.Printf("Copied: %d elements\n", n1)

	// Example 2: Copying to same-size slice (copies all elements)
	fmt.Println("\n--- Example 2: Copying to same-size slice ---")
	source2 := []string{"dog", "cat", "bird"}
	dest2 := make([]string, 3)

	n2 := copy(dest2, source2)
	fmt.Printf("Source: %v (len=%d)\n", source2, len(source2))
	fmt.Printf("Dest:   %v (len=%d)\n", dest2, len(dest2))
	fmt.Printf("Copied: %d elements\n", n2)

	// Example 3: Copying to larger slice (copies only source elements)
	fmt.Println("\n--- Example 3: Copying to larger slice ---")
	source3 := []string{"red", "green"}
	dest3 := make([]string, 5) // larger than source

	n3 := copy(dest3, source3)
	fmt.Printf("Source: %v (len=%d)\n", source3, len(source3))
	fmt.Printf("Dest:   %v (len=%d)\n", dest3, len(dest3))
	fmt.Printf("Copied: %d elements\n", n3)
	fmt.Println("Note: Remaining elements in dest are zero values")

	// Example 4: Copying to smaller slice (partial copy)
	fmt.Println("\n--- Example 4: Copying to smaller slice (partial copy) ---")
	source4 := []string{"alpha", "beta", "gamma", "delta", "epsilon"}
	dest4 := make([]string, 2) // smaller than source

	n4 := copy(dest4, source4)
	fmt.Printf("Source: %v (len=%d)\n", source4, len(source4))
	fmt.Printf("Dest:   %v (len=%d)\n", dest4, len(dest4))
	fmt.Printf("Copied: %d elements\n", n4)
	fmt.Println("Note: Only the first 2 elements were copied")

	// Example 5: Common pitfall - using capacity instead of length
	fmt.Println("\n--- Example 5: Capacity vs Length pitfall ---")
	source5 := []int{10, 20, 30, 40, 50}
	dest5 := make([]int, 0, 10) // length is 0, but capacity is 10

	n5 := copy(dest5, source5)
	fmt.Printf("Source: %v (len=%d)\n", source5, len(source5))
	fmt.Printf("Dest:   %v (len=%d, cap=%d)\n", dest5, len(dest5), cap(dest5))
	fmt.Printf("Copied: %d elements\n", n5)
	fmt.Println("Note: Even though capacity is 10, length is 0, so nothing is copied!")

	// Solution: ensure destination has correct length
	fmt.Println("\n--- Example 6: Correct approach with length ---")
	source6 := []int{10, 20, 30, 40, 50}
	dest6 := make([]int, len(source6)) // length matches source

	n6 := copy(dest6, source6)
	fmt.Printf("Source: %v (len=%d)\n", source6, len(source6))
	fmt.Printf("Dest:   %v (len=%d, cap=%d)\n", dest6, len(dest6), cap(dest6))
	fmt.Printf("Copied: %d elements\n", n6)
}

Important: Copy uses the length (not capacity) of the destination slice to determine how many elements to copy.

Copy And Arrays

The copy() built-in function only works with slices, not arrays. However, you can use slice notation (array[:]) to create a slice view of an array.


func main() {
	// The copy() built-in function works with slices, not arrays

	// Example 1: Attempting to copy FROM an array (compile error)
	// Uncomment to see the error:
	// a := [2]int{1, 2}
	// s := []int{3, 4}
	// copy(s, a) // Compile error: cannot use a (type [2]int) as type []int

	// Example 2: Attempting to copy TO an array (compile error)
	// Uncomment to see the error:
	// s := []int{1, 2}
	// a := [2]int{3, 4}
	// copy(a, s) // Compile error: first argument to copy should be a slice

	// Workaround: Convert array to slice using slice notation
	fmt.Println("--- Workaround: Array to Slice Conversion ---")

	// Copy FROM an array: use array[:] to create a slice view
	sourceArray := [4]int{10, 20, 30, 40}
	destSlice := make([]int, 4)

	// Convert array to slice using [:]
	copy(destSlice, sourceArray[:])
	fmt.Printf("Source array: %v\n", sourceArray)
	fmt.Printf("Dest slice:   %v\n", destSlice)

	// Copy TO an array: use array[:] to create a slice view
	fmt.Println("\n--- Copy to an array using slice conversion ---")
	sourceSlice := []int{100, 200, 300}
	destArray := [3]int{}

	// Convert array to slice using [:] to allow copy
	copy(destArray[:], sourceSlice)
	fmt.Printf("Source slice: %v\n", sourceSlice)
	fmt.Printf("Dest array:   %v\n", destArray)

	// Important: The slice view shares the array's underlying data
	fmt.Println("\n--- Understanding array[:] creates a slice view ---")
	arr := [3]string{"a", "b", "c"}
	sliceView := arr[:]

	fmt.Printf("Original array: %v (type: %T)\n", arr, arr)
	fmt.Printf("Slice view:     %v (type: %T)\n", sliceView, sliceView)

	// Modifying through the slice view affects the array
	sliceView[0] = "MODIFIED"
	fmt.Printf("After modifying slice view:\n")
	fmt.Printf("Array:      %v\n", arr)
	fmt.Printf("Slice view: %v\n", sliceView)
}

Note: Using array[:] creates a slice that shares the array’s underlying data.

Slices Package

In release 1.21, the slices package was introduced. It includes many useful functions for sorting, managing, and searching slices. While covering each and every function in the package is beyond the scope of this training, we will cover the more common ones used.

Clip

Clip removes unused capacity from the slice. This can be used to release memory back to the runtime if you have had a large amount if items in a slice and have since reduced the length (or in use items).


fmt.Printf("len: %d, capacity: %d\n", len(ints), cap(ints))
ints = slices.Clip(ints)
fmt.Printf("len: %d, capacity: %d\n", len(ints), cap(ints))

Output:


len: 25, capacity: 500
len: 25, capacity: 25

Clone

Clone returns a copy of the slice. All values are copied by assignment, which means that this is considered a shallow copy of elements in the slice. A shallow copy means that the original memory addresses are still in use, so it’s possible you might still make changes to the original data. While this normally isn’t a problem, it’s important to keep in mind if you want a truly independent copy of the data.


original := []int{0, 1, 2, 4, 5}
cloned := slices.Clone(original)
cloned[0] = 9
cloned[1] = 8
cloned[2] = 7
cloned[3] = 6
cloned[4] = 5
fmt.Printf("original: %v\n", original)
fmt.Printf("cloned  : %v\n", cloned)

Output:


original: [0 1 2 4 5]
cloned  : [9 8 7 6 5]

Clone With Sub Slices

Clone is especially useful when taking a subslice of a slice. Using slices.Clone instead of directly slicing will allow you to mututate (or make changes) to the subslice without directly affecting the parent slice you sliced the data from.

Take the following example. If you take a subslice of the parent, and make changes to the sliced elements, it will also make changes to the parent slice as well:


original := []int{0, 1, 2, 4, 5}
cloned := original[1:4]
cloned[0] = 9
cloned[1] = 8
cloned[2] = 7
fmt.Printf("original: %v\n", original)
fmt.Printf("cloned  : %v\n", cloned)

Output:


original: [0 9 8 7 5]
cloned  : [9 8 7]

To avoid this, you can use the slices.Clone function and wrap the subslice to make a copy instead of a reference and no longer affect the parent if you make changes to the child:


original := []int{0, 1, 2, 4, 5}
cloned := slices.Clone(original[1:4]) // <-- clone the subslice
cloned[0] = 9
cloned[1] = 8
cloned[2] = 7
fmt.Printf("original: %v\n", original)
fmt.Printf("cloned  : %v\n", cloned)

Output:


original: [0 1 2 4 5]
cloned  : [9 8 7]

Notice that the parent slice is no longer affected by the changes made by the subslice.

BinarySearch

BinarySearch searches for a target value in a sorted slice using binary search. It returns the position where the target is found, or where it would be inserted to maintain the sorted order, and a boolean indicating whether the target was found.

Important: The slice must be sorted in ascending order for BinarySearch to work correctly.


// Binary search requires a sorted slice
numbers := []int{1, 3, 5, 7, 9, 11, 13, 15}

// Search for existing value
index, found := slices.BinarySearch(numbers, 7)
fmt.Printf("Search for 7: index=%d, found=%v\n", index, found)

// Search for non-existing value
index, found = slices.BinarySearch(numbers, 8)
fmt.Printf("Search for 8: index=%d, found=%v\n", index, found)

// When not found, index indicates where to insert
fmt.Printf("If we insert 8 at index %d: %v\n", index,
	slices.Insert(numbers, index, 8))

Output:


Search for 7: index=3, found=true
Search for 8: index=4, found=false
If we insert 8 at index 4: [1 3 5 7 8 9 11 13 15]

Compact

Compact replaces consecutive runs of equal elements with a single copy.

The documentation doesn’t state that the slice already needs to be sorted for the function to work. Upon a quick test, it shows that compact does in fact require a sorted slice to work properly. Luckily, the slices package made sorting very easy as well.


strings := []string{"Apple", "Orange", "Apple", "Banana"}
ints := []int{0, 1, 1, 2, 0, 5, 7, 5}
strings = slices.Compact(strings)
ints = slices.Compact(ints)
fmt.Printf("Not Sorted - Compact does not work\n")
fmt.Println(strings)
fmt.Println(ints)

fmt.Printf("\nSorted - Compact now works as expected\n")
slices.Sort(ints)
slices.Sort(strings)
strings = slices.Compact(strings)
ints = slices.Compact(ints)
fmt.Println(strings)
fmt.Println(ints)

Output:


Not Sorted - Compact does not work
[Apple Orange Apple Banana]
[0 1 2 0 5 7 5]

Sorted - Compact now works as expected
[Apple Banana Orange]
[0 1 2 5 7]

Compare

Compare compares two slices element-wise and returns an integer comparing the two slices. It returns -1 if the first slice is less than the second, 0 if they are equal, and +1 if the first slice is greater.


slice1 := []int{1, 2, 3}
slice2 := []int{1, 2, 4}
slice3 := []int{1, 2, 3, 4}

// Compare returns:
// -1 if slice1 < slice2
//  0 if slice1 == slice2
// +1 if slice1 > slice2
fmt.Printf("Compare([1,2,3], [1,2,4]): %d\n", slices.Compare(slice1, slice2))
fmt.Printf("Compare([1,2,3], [1,2,3]): %d\n", slices.Compare(slice1, slice1))
fmt.Printf("Compare([1,2,4], [1,2,3]): %d\n", slices.Compare(slice2, slice1))
fmt.Printf("Compare([1,2,3], [1,2,3,4]): %d\n", slices.Compare(slice1, slice3))

Output:


Compare([1,2,3], [1,2,4]): -1
Compare([1,2,3], [1,2,3]): 0
Compare([1,2,4], [1,2,3]): 1
Compare([1,2,3], [1,2,3,4]): -1

Contains

Contains reports if the provided value exists in the slice. This is useful to detect the presence of an element in a slice.


s := []string{"Apple", "Banana", "Orange"}
fmt.Println("Found Apple?", slices.Contains(s, "Apple"))
fmt.Println("Found Banana?", slices.Contains(s, "Banana"))
fmt.Println("Found Strawberry?", slices.Contains(s, "Strawberry"))

Output:


Found Apple? true
Found Banana? true
Found Strawberry? false

Equal

Equal reports whether two slices are equal: the same length and all elements are equal. If the lengths are different, Equal returns false.


slice1 := []int{1, 2, 3, 4, 5}
slice2 := []int{1, 2, 3, 4, 5}
slice3 := []int{1, 2, 3, 4, 6}

fmt.Println("slice1 == slice2:", slices.Equal(slice1, slice2))
fmt.Println("slice1 == slice3:", slices.Equal(slice1, slice3))

Output:


slice1 == slice2: true
slice1 == slice3: false

Index

Index returns the index of the first occurrence of the searched value in the slice. It will return -1 if the value is not present.


s := []string{"Apple", "Banana", "Orange"}
fmt.Println("Index for Apple:      ", slices.Index(s, "Apple"))
fmt.Println("Index for Banana:     ", slices.Index(s, "Banana"))
fmt.Println("Index for Strawberry: ", slices.Index(s, "Strawberry"))

Output:


Index for Apple:       0
Index for Banana:      1
Index for Strawberry:  -1

Insert

Insert inserts the value at the index provided. The slice will grow as needed to make room for the inserted values.


s := []string{"Apple", "Banana", "Orange"}
s = slices.Insert(s, 2, "Grape")
fmt.Println(s)

Output:


[Apple Banana Grape Orange]

Grow

Grow increases the slice’s capacity, if necessary, to guarantee space for another n elements. This is useful when you know you’ll be appending many elements and want to avoid multiple reallocations.


numbers := []int{1, 2, 3}
fmt.Printf("Original: len=%d, cap=%d, slice=%v\n",
	len(numbers), cap(numbers), numbers)

// Grow increases capacity by at least n elements
numbers = slices.Grow(numbers, 10)
fmt.Printf("After Grow(10): len=%d, cap=%d, slice=%v\n",
	len(numbers), cap(numbers), numbers)

// This is useful when you know you'll be appending many elements
// It pre-allocates capacity to avoid multiple reallocations

Output:


Original: len=3, cap=3, slice=[1 2 3]
After Grow(10): len=3, cap=14, slice=[1 2 3]

Max

Max returns the max value in the slice. It will panic if the slice is empty. The slice does NOT need to be sorted for this function to work.


strings := []string{"Apple", "Orange", "Apple", "Banana"}
ints := []int{0, 1, 9, 1, 2, 0, 5, 7, 5}
fmt.Println("Max value for strings slice is", slices.Max(strings))
fmt.Println("Max value for ints slice is", slices.Max(ints))

Output:


Max value for strings slice is Orange
Max value for ints slice is 9

Min

Min returns the minimal value in the slice. It will panic if the slice is empty. The slice does NOT need to be sorted for this function to work.


strings := []string{"Apple", "Orange", "Apple", "Banana"}
ints := []int{0, 1, 9, 1, 2, 0, 5, 7, 5}
fmt.Println("Min value for strings slice is", slices.Min(strings))
fmt.Println("Min value for ints slice is", slices.Min(ints))

Output:


Min value for strings slice is Apple
Min value for ints slice is 0

Replace

Replace replaces the selected section of the slice with the provided values.

Note: You do not need to replace the with the exact number of elements. You can use less or more as necessary. This would allow you to replace 2 elements with 1 element, or 1 element with 3 elements, etc.


strings := []string{"Apple", "Orange", "Apple", "Banana"}
ints := []int{0, 1, 9, 1, 2, 0, 5, 7, 5}

strings = slices.Replace(strings, 0, 2, "Apple", "Grape")
ints = slices.Replace(ints, 0, 2, 9, 9)

fmt.Println(strings)
fmt.Println(ints)

// Replace and insert at the same time
strings = slices.Replace(strings, 0, 0, "Prune", "Pineapple")
ints = slices.Replace(ints, 0, 0, -1, -1)

fmt.Println(strings)
fmt.Println(ints)

Output:


[Apple Grape Apple Banana]
[9 9 9 1 2 0 5 7 5]
[Prune Pineapple Apple Grape Apple Banana]
[-1 -1 9 9 9 1 2 0 5 7 5]

Reverse

Reverse reverses the elements of the slice in place. This modifies the original slice.


numbers := []int{1, 2, 3, 4, 5}
words := []string{"Apple", "Banana", "Cherry"}

fmt.Println("Before reverse:")
fmt.Println("  numbers:", numbers)
fmt.Println("  words:  ", words)

slices.Reverse(numbers)
slices.Reverse(words)

fmt.Println("\nAfter reverse:")
fmt.Println("  numbers:", numbers)
fmt.Println("  words:  ", words)

Output:


Before reverse:
  numbers: [1 2 3 4 5]
  words:   [Apple Banana Cherry]

After reverse:
  numbers: [5 4 3 2 1]
  words:   [Cherry Banana Apple]

Sort

Sort sorts a slice of any ordered type in ascending order. Probably one of the most useful functions added to the standard library since Go was released in 2012!


strings := []string{"Apple", "Orange", "Apple", "Banana"}
ints := []int{0, 1, 1, 2, 0, 5, 7, 5}
fmt.Println(strings)
fmt.Println(ints)

// Sort strings
slices.Sort(ints)
slices.Sort(strings)

fmt.Println(strings)
fmt.Println(ints)

Output:


[Apple Orange Apple Banana]
[0 1 1 2 0 5 7 5]
[Apple Apple Banana Orange]
[0 0 1 1 2 5 5 7]

Concat

Concat returns a new slice concatenating the passed in slices. This provides a cleaner alternative to chained append calls.


s1 := []string{"apple", "banana"}
s2 := []string{"cherry", "date"}
s3 := []string{"elderberry"}

// Before Go 1.21, you would need to chain append calls:
// combined := append(append(s1, s2...), s3...)

// With slices.Concat, this becomes cleaner:
combined := slices.Concat(s1, s2, s3)

fmt.Println("s1:", s1)
fmt.Println("s2:", s2)
fmt.Println("s3:", s3)
fmt.Println("combined:", combined)

Output:


s1: [apple banana]
s2: [cherry date]
s3: [elderberry]
combined: [apple banana cherry date elderberry]

Added in Go 1.21

Repeat

Repeat returns a new slice that repeats the provided slice the given number of times.


pattern := []string{"Go", "is", "fun"}

// Repeat the pattern 3 times
repeated := slices.Repeat(pattern, 3)

fmt.Println("pattern:", pattern)
fmt.Println("repeated:", repeated)

// Useful for creating initialized slices
zeros := slices.Repeat([]int{0}, 5)
fmt.Println("zeros:", zeros)

// Or creating separator patterns
separator := slices.Repeat([]string{"-"}, 3)
fmt.Println("separator:", separator)

Output:


pattern: [Go is fun]
repeated: [Go is fun Go is fun Go is fun]
zeros: [0 0 0 0 0]
separator: [- - -]

Added in Go 1.23

Iterator Functions (Go 1.23+)

Go 1.23 introduced iterator support throughout the standard library, and the slices package gained several powerful iterator-based functions. These functions work with Go’s range-over-function feature and integrate with the iter package.

All

All returns an iterator over index-value pairs in the slice. This is similar to using range directly, but returns an iterator that can be passed to other iterator-consuming functions.


fruits := []string{"apple", "banana", "cherry"}

// slices.All returns an iterator over index-value pairs
// This is similar to range, but returns an iterator
fmt.Println("Using slices.All:")
for i, v := range slices.All(fruits) {
	fmt.Printf("  index %d: %s\n", i, v)
}

// Useful when you need to pass an iterator to another function
// or when working with iterator-based APIs

Output:


Using slices.All:
  index 0: apple
  index 1: banana
  index 2: cherry

Values

Values returns an iterator over just the values in the slice, without indices. This is useful when you only care about the values and want to pass them to iterator-consuming functions.


numbers := []int{10, 20, 30, 40}

// slices.Values returns an iterator over values only (no indices)
fmt.Println("Using slices.Values:")
for v := range slices.Values(numbers) {
	fmt.Printf("  value: %d\n", v)
}

// This is useful when you only care about values
// and want to pass them to iterator-consuming functions

Output:


Using slices.Values:
  value: 10
  value: 20
  value: 30
  value: 40

Backward

Backward returns an iterator that yields index-value pairs in reverse order, from last to first. Unlike slices.Reverse, this doesn’t modify the original slice.


items := []string{"first", "second", "third", "fourth"}

// slices.Backward returns an iterator that yields
// index-value pairs in reverse order
fmt.Println("Using slices.Backward:")
for i, v := range slices.Backward(items) {
	fmt.Printf("  index %d: %s\n", i, v)
}

// Unlike slices.Reverse, this doesn't modify the original slice
fmt.Println("Original slice unchanged:", items)

Output:


Using slices.Backward:
  index 3: fourth
  index 2: third
  index 1: second
  index 0: first
Original slice unchanged: [first second third fourth]

Collect

Collect collects values from an iterator into a new slice. This is the inverse of Values - it takes an iterator and produces a slice.


// slices.Collect collects values from an iterator into a slice
m := map[string]int{
	"apple":  1,
	"banana": 2,
	"cherry": 3,
}

// Get all keys from a map as a slice using maps.Keys iterator
keys := slices.Collect(maps.Keys(m))
slices.Sort(keys) // Sort for consistent output
fmt.Println("Keys:", keys)

// Get all values from a map as a slice using maps.Values iterator
values := slices.Collect(maps.Values(m))
slices.Sort(values) // Sort for consistent output
fmt.Println("Values:", values)

Output:


Keys: [apple banana cherry]
Values: [1 2 3]

AppendSeq

AppendSeq appends values from an iterator to an existing slice and returns the result. Unlike Collect, it preserves existing elements.


existing := []string{"existing1", "existing2"}

m := map[string]int{
	"new1": 1,
	"new2": 2,
}

// AppendSeq appends values from an iterator to an existing slice
combined := slices.AppendSeq(existing, maps.Keys(m))
slices.Sort(combined) // Sort for consistent output
fmt.Println("Combined:", combined)

// Unlike Collect, AppendSeq preserves existing elements
// This is useful when building up a slice incrementally

Output:


Combined: [existing1 existing2 new1 new2]

Sorted

Sorted collects values from an iterator and returns them as a sorted slice. This combines Collect and Sort in a single operation.


m := map[string]int{
	"cherry": 3,
	"apple":  1,
	"banana": 2,
}

// slices.Sorted collects values from an iterator and returns
// them as a sorted slice - combines Collect and Sort in one step
sortedKeys := slices.Sorted(maps.Keys(m))
fmt.Println("Sorted keys:", sortedKeys)

// Compare with doing it manually:
// keys := slices.Collect(maps.Keys(m))
// slices.Sort(keys)

// There's also SortedFunc for custom comparison
// and SortedStableFunc for stable sorting with custom comparison

Output:


Sorted keys: [apple banana cherry]

Related functions: SortedFunc for custom comparison and SortedStableFunc for stable sorting with custom comparison.

Chunk

Chunk returns an iterator over consecutive sub-slices of up to n elements each. The last chunk may have fewer than n elements if the slice length isn’t evenly divisible.


numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

// slices.Chunk returns an iterator over contiguous sub-slices
// of up to n elements each
fmt.Println("Original:", numbers)
fmt.Println("Chunks of 3:")
for chunk := range slices.Chunk(numbers, 3) {
	fmt.Printf("  %v\n", chunk)
}

// Useful for batch processing
fmt.Println("Chunks of 4:")
for chunk := range slices.Chunk(numbers, 4) {
	fmt.Printf("  %v\n", chunk)
}

Output:


Original: [1 2 3 4 5 6 7 8 9 10]
Chunks of 3:
  [1 2 3]
  [4 5 6]
  [7 8 9]
  [10]
Chunks of 4:
  [1 2 3 4]
  [5 6 7 8]
  [9 10]

Summary

As you can see, the slices package adds many useful functions that most Go developers will be using daily. Since Go 1.21, the package has continued to grow:

  • Go 1.21: Added Concat for cleaner slice concatenation
  • Go 1.23: Added Repeat for repeating slices, plus iterator functions (All, Values, Backward, Collect, AppendSeq, Sorted, SortedFunc, SortedStableFunc, Chunk)

Below, I’ll show an example of something that previously would have required a lot of code (and somewhat hard to understand code as well). For this example, we will take a slice that has duplicate items and:

  1. Replace all occurrences of “snake” with “gopher”
  2. Sort the slice
  3. Compact the slice
  4. Clip the slice to free up any space (not necessary on a slice this size, but using it to show it would be useful on a larger scale operation).

Note: If this wasn’t an example, I would have sorted and compacted the slice first to decrease the amount of work that the Replace function had to do. But I wanted to make this a fun example to show off the slices package!


animals := []string{
	"snake",
	"guinea pig",
	"elephant",
	"deer",
	"snake",
	"dung beetle",
	"okapi",
	"badger",
	"lovebird",
	"coyote",
	"camel",
	"antelope",
	"marten",
	"rabbit",
	"cougar",
	"cow",
	"finch",
	"snake",
	"reindeer",
	"rat",
	"moose",
	"crocodile",
	"snake",
	"giraffe",
	"gnu",
}
fmt.Printf("Original Slice:\nlen: %d, \ncap: %d, \nelements: %v\n", len(animals), cap(animals), animals)

i := slices.Index(animals, "snake")
for i > -1 {
	animals = slices.Replace(animals, i, i+1, "gopher")
	i = slices.Index(animals, "snake")
}

slices.Sort(animals)

animals = slices.Compact(animals)

animals = slices.Clip(animals)

fmt.Printf("Final Slice:\nlen: %d, \ncap: %d, \nelements: %v\n", len(animals), cap(animals), animals)

Output:


Original Slice:
len: 25,
cap: 25,
elements: [snake guinea pig elephant deer snake dung beetle okapi badger lovebird coyote camel antelope marten rabbit cougar cow finch snake reindeer rat moose crocodile snake giraffe gnu]
Final Slice:
len: 22,
cap: 22,
elements: [antelope badger camel cougar cow coyote crocodile deer dung beetle elephant finch giraffe gnu gopher guinea pig lovebird marten moose okapi rabbit rat reindeer]

Performance Benchmarks

Understanding the performance characteristics of slice operations helps you write more efficient code. Pre-allocating capacity and choosing the right approach can significantly impact performance.


func main() {
	fmt.Println("This package contains benchmark tests.")
	fmt.Println("Run benchmarks with: go test -bench=.")
	fmt.Println()
	fmt.Println("Example commands:")
	fmt.Println("  go test -bench=. -benchmem")
	fmt.Println("  go test -bench=BenchmarkAppend -benchmem")
	fmt.Println("  go test -bench=. -benchtime=10s")
	fmt.Println()
	fmt.Println("Key findings from benchmarks:")
	fmt.Println()
	fmt.Println("1. Pre-allocation with make([]T, 0, capacity) is significantly faster")
	fmt.Println("   than growing a slice through repeated append operations.")
	fmt.Println()
	fmt.Println("2. Using make([]T, length) and direct indexing is fastest when you")
	fmt.Println("   know the final size, avoiding append overhead entirely.")
	fmt.Println()
	fmt.Println("3. As slice size increases, the performance impact of not pre-allocating")
	fmt.Println("   becomes more severe due to repeated memory reallocations.")
	fmt.Println()
	fmt.Println("4. For iteration, range loops are generally as fast or faster than")
	fmt.Println("   index-based loops and are more idiomatic Go.")
	fmt.Println()
	fmt.Println("Best practices:")
	fmt.Println("- Pre-allocate capacity when you know approximate final size")
	fmt.Println("- Use make([]T, length) when size is exactly known")
	fmt.Println("- Prefer range loops for readability without performance penalty")
}

Run the benchmarks to see the actual performance differences:

cd src/examples/benchmarks
go test -bench=. -benchmem

Key findings:

  • Pre-allocating with make([]T, 0, capacity) is significantly faster than growing through repeated appends
  • Using make([]T, length) with direct indexing is fastest when size is known
  • The performance impact grows worse as slice size increases
  • Range and index loops are optimized identically by the compiler through Bounds Check Elimination (BCE), resulting in equivalent performance. Choose range for idiomatic, readable code

Slice Tricks

In addition to the slices package, there is also a wiki that includes many additional patterns that may be useful when using slices:

Slice Tricks

Exercise (15 Mins)


package main

import "fmt"

func main() {
	parents := []string{"Carol", "Mike"}
	kids := []string{"Marcia", "Jan", "Cindy", "Greg", "Peter", "Bobby"}

	// TODO: Create a new slice called family by joining the parents and kids slice together

	// TODO: Print out the value of `family`

	// TODO: Print the length and capacity of the new slice.

	// TODO: Create a new slice called "boys" by taking the last 3 elements of the slice you just made.
	// HINT: slice[starting_index : (starting_index + length)], or use the shorthand version of slicing

	// TODO: Print out the value of the new slice `boys` that you just created

	// TODO: Fix the following bugs
	extras := make([]string, 1, 0)
	extras[1] = "Alice"
	fmt.Println(extras)
}

Solution


package main

import "fmt"

func main() {
	parents := []string{"Carol", "Mike"}
	kids := []string{"Marcia", "Jan", "Cindy", "Greg", "Peter", "Bobby"}

	// TODO: Create a new slice called family by joining the parents and kids slice together
	family := append(parents, kids...)

	// TODO: Print out the value of `family`
	fmt.Println(family)

	// TODO: Print the length and capacity of the new slice.
	fmt.Printf("len(family): %d, cap(family): %d\n", len(family), cap(family))

	// TODO: Create a new slice called "boys" by taking the last 3 elements of the slice you just made.
	// HINT: slice[starting_index : (starting_index + length)], or use the shorthand version of slicing

	boys := family[len(family)-3:] // or boys := family[5:]

	// TODO: Print out the value of the new slice `boys` that you just created
	fmt.Println(boys)

	// TODO: Fix the following bugs
	extras := make([]string, 1, 1)
	extras[0] = "Alice"

	fmt.Println(extras)
}

You can also use copy and potentially avoid allocations with the following solution:


package main

import "fmt"

func main() {
	parents := []string{"Carol", "Mike"}
	kids := []string{"Marcia", "Jan", "Cindy", "Greg", "Peter", "Bobby"}

	// TODO: Create a new slice called family by joining the parents and kids slice together
	family := make([]string, len(parents)+len(kids))
	copy(family, parents)
	copy(family[len(parents):], kids)

	// TODO: Print out the value of `family`
	fmt.Println(family)

	// TODO: Print the length and capacity of the new slice.
	fmt.Printf("len(family): %d, cap(family): %d\n", len(family), cap(family))

	// TODO: Create a new slice called "boys" by taking the last 3 elements of the slice you just made.
	// HINT: slice[starting_index : (starting_index + length)], or use the shorthand version of slicing

	boys := family[len(family)-3:] // or boys := family[5:]

	// TODO: Print out the value of the new slice `boys` that you just created
	fmt.Println(boys)

	// TODO: Fix the following bugs
	extras := make([]string, 1, 1)
	extras[0] = "Alice"

	fmt.Println(extras)
}