Go > Collections > Arrays and Slices > Slice basics

Understanding Go Slices: A Comprehensive Guide

This snippet demonstrates the fundamental concepts of slices in Go, including creation, slicing, appending, and copying. Slices are a key data structure in Go, offering flexibility and efficiency when working with collections of data.

Slice Creation

This section showcases different ways to create slices in Go. You can create a slice from an existing array using the slicing operator `[start:end]`. Alternatively, you can use the `make` function to create a slice with a specified length and capacity. An empty slice can be declared using `[]int{}` and a nil slice using `var []int`.

package main

import "fmt"

func main() {
	// Creating a slice from an array
	arr := [5]int{1, 2, 3, 4, 5}
	slice1 := arr[1:4] // Creates a slice from index 1 (inclusive) to 4 (exclusive)

	fmt.Println("Slice from array:", slice1) // Output: [2 3 4]

	// Creating a slice using make
	slice2 := make([]int, 3)      // Creates a slice of length 3 with capacity 3
	slice3 := make([]int, 3, 5)   // Creates a slice of length 3 with capacity 5

	slice2[0] = 10
	slice2[1] = 20
	slice2[2] = 30

	fmt.Println("Slice using make (len=3, cap=3):", slice2) // Output: [10 20 30]
	fmt.Println("Slice using make (len=3, cap=5):", slice3) // Output: [0 0 0]

	// Creating an empty slice
	slice4 := []int{}
	fmt.Println("Empty slice:", slice4) // Output: []

	// Creating a nil slice
	var slice5 []int
	fmt.Println("Nil slice:", slice5 == nil) // Output: true
}

Slice Slicing

Slicing allows you to create a new slice that refers to a portion of the original slice. The `[start:end]` notation specifies the start and end indices of the desired portion. If `start` is omitted, it defaults to 0. If `end` is omitted, it defaults to the length of the slice. Critically, slices are *references*. Modifying a sub-slice can modify the underlying array.

package main

import "fmt"

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

	fmt.Println("Original slice:", slice)

	// Slicing from index 1 to 3 (exclusive)
	subSlice1 := slice[1:3]
	fmt.Println("Slice [1:3]:", subSlice1) // Output: [2 3]

	// Slicing from index 2 to the end
	subSlice2 := slice[2:]
	fmt.Println("Slice [2:]:", subSlice2) // Output: [3 4 5]

	// Slicing from the beginning to index 3 (exclusive)
	subSlice3 := slice[:3]
	fmt.Println("Slice [:3]:", subSlice3) // Output: [1 2 3]

	// Creating a copy of the entire slice
	subSlice4 := slice[:]
	fmt.Println("Slice [:]:", subSlice4) // Output: [1 2 3 4 5]
}

Appending to a Slice

The `append` function allows you to add elements to the end of a slice. If the slice has enough capacity, the new elements are added to the existing underlying array. If the capacity is not sufficient, a new underlying array is allocated, and the existing elements are copied to the new array. Note the use of `...` to unpack another slice when appending.

package main

import "fmt"

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

	fmt.Println("Original slice:", slice)
	fmt.Println("Length:", len(slice), "Capacity:", cap(slice))

	// Appending a single element
	slice = append(slice, 4)
	fmt.Println("Slice after appending 4:", slice)
	fmt.Println("Length:", len(slice), "Capacity:", cap(slice))

	// Appending multiple elements
	slice = append(slice, 5, 6, 7)
	fmt.Println("Slice after appending 5, 6, 7:", slice)
	fmt.Println("Length:", len(slice), "Capacity:", cap(slice))

	// Appending another slice
	anotherSlice := []int{8, 9, 10}
	slice = append(slice, anotherSlice...)
	fmt.Println("Slice after appending anotherSlice:", slice)
	fmt.Println("Length:", len(slice), "Capacity:", cap(slice))
}

Copying Slices

The `copy` function copies elements from a source slice to a destination slice. The number of elements copied is the minimum of the lengths of the source and destination slices. It's essential to create a destination slice of the appropriate size before copying, otherwise you may not copy all desired data. `copy` does *not* create a slice that refers to the underlying array; it creates a brand new copy.

package main

import "fmt"

func main() {
	sourceSlice := []int{1, 2, 3, 4, 5}
	destSlice := make([]int, len(sourceSlice))

	numCopied := copy(destSlice, sourceSlice)

	fmt.Println("Source slice:", sourceSlice)
	fmt.Println("Destination slice:", destSlice)
	fmt.Println("Number of elements copied:", numCopied)

	// Copying a smaller slice into a larger one
	smallerSource := []int{1, 2}
	largerDest := make([]int, 5)
	numCopied = copy(largerDest, smallerSource)
	fmt.Println("Smaller source, larger dest:", largerDest)
	fmt.Println("Number of elements copied:", numCopied)

	// Copying a larger slice into a smaller one
	largerSource := []int{1, 2, 3, 4, 5}
	smallerDest := make([]int, 3)
	numCopied = copy(smallerDest, largerSource)
	fmt.Println("Larger source, smaller dest:", smallerDest)
	fmt.Println("Number of elements copied:", numCopied)
}

Concepts Behind the Snippet

Slices are built on top of arrays. A slice is a descriptor that contains a pointer to an underlying array, a length (the number of elements the slice contains), and a capacity (the number of elements in the underlying array starting from the slice's first element). When appending to a slice and the capacity is exceeded, a new, larger array is allocated, and the elements are copied. This reallocation can be expensive, so understanding capacity is crucial for performance.

Real-Life Use Case

Slices are used extensively in Go for various purposes. For example, when reading data from a file or a network connection, you often use a slice to store the data as it arrives. They are also very common when dealing with JSON or YAML parsing or generating.

Best Practices

  • Pre-allocate slices using `make` with the expected length or capacity if you know the size beforehand. This can improve performance by avoiding reallocations.
  • Be mindful of the capacity of your slices, especially when appending. Consider increasing the capacity in larger increments to avoid frequent reallocations.
  • When copying slices, ensure the destination slice has enough space to accommodate the copied elements.
  • Avoid unnecessary copying of slices, as it can impact performance. If you only need to read the data, consider passing the slice by reference.

Interview Tip

Be prepared to explain the difference between the length and capacity of a slice. Also, be able to describe how appending to a slice works and what happens when the capacity is exceeded. Understanding how slices are backed by arrays is also a common interview question.

When to use them

Use slices when you need a dynamically sized sequence of elements. Slices are more flexible than arrays because their size can be adjusted at runtime. If you need a fixed-size sequence, use arrays. However, in most real-world scenarios, slices are the preferred choice.

Memory Footprint

A slice's memory footprint consists of the size of the slice descriptor (which includes the pointer to the underlying array, length, and capacity) and the size of the underlying array itself. When a slice is created from an existing array, it shares the same underlying array, so it doesn't allocate new memory for the elements. However, when appending to a slice and the capacity is exceeded, a new array is allocated, potentially doubling the memory usage.

Alternatives

Alternatives to slices include:

  • Arrays: Use arrays when you need a fixed-size sequence of elements.
  • Linked lists: Use linked lists when you need to insert or delete elements frequently, but random access is not a primary requirement.
  • Maps: Use maps when you need to store key-value pairs.
The appropriate choice depends on the specific requirements of your application.

Pros

  • Dynamically sized: Slices can grow and shrink as needed.
  • Efficient: Slices provide efficient access to elements using indexing.
  • Flexible: Slices can be easily sliced and copied.
  • Widely used: Slices are a fundamental data structure in Go and are used extensively in the standard library.

Cons

  • Underlying array: Slices are backed by arrays, which can lead to unexpected behavior if you're not careful when modifying slices that share the same underlying array.
  • Reallocation: Appending to a slice can trigger reallocation of the underlying array, which can be expensive.
  • Not comparable: Slices cannot be directly compared using the == operator. You need to use the `reflect.DeepEqual` function or iterate over the elements to compare them.

FAQ

  • What is the difference between length and capacity of a slice?

    The length of a slice is the number of elements it currently holds. The capacity is the number of elements the underlying array can hold from the starting index of the slice.
  • How can I create a copy of a slice?

    You can use the `copy` function to create a copy of a slice. Alternatively, you can create a new slice and copy the elements manually using a loop.
  • What happens when I append to a slice and the capacity is exceeded?

    When the capacity is exceeded, a new underlying array is allocated with a larger capacity, and the existing elements are copied to the new array.