Module: Data Structures

Slices

Go Programming: Slices

Slices are a fundamental data structure in Go, providing a dynamic, flexible way to work with sequences of elements. They are built on top of arrays but offer more convenience and power.

What are Slices?

  • Dynamic Size: Unlike arrays, which have a fixed size defined at compile time, slices can grow or shrink dynamically at runtime.
  • Reference Type: Slices are reference types. This means that when you assign a slice to a new variable, you're copying the slice header (pointer, length, and capacity), not the underlying array data. Changes made through one slice can affect other slices that share the same underlying array.
  • Underlying Array: A slice is essentially a descriptor that points to a contiguous segment of an underlying array. This array can be created specifically for the slice, or it can be a portion of an existing array.
  • Length and Capacity:
    • Length: The number of elements currently stored in the slice. len(slice)
    • Capacity: The maximum number of elements the slice can hold without reallocating the underlying array. cap(slice)

Creating Slices

There are several ways to create slices:

  1. Slice Literals:

    // Create a slice of integers
    mySlice := []int{1, 2, 3, 4, 5}
    
    // Create an empty slice of strings
    emptySlice := []string{}
    
  2. Using make():

    // Create a slice of integers with length 3 and capacity 5
    mySlice := make([]int, 3, 5) // [0 0 0] (length 3, capacity 5)
    
    // Create a slice of strings with length and capacity 4
    stringSlice := make([]string, 4) // ["", "", "", ""] (length 4, capacity 4)
    
  3. Slicing an Array:

    myArray := [5]int{10, 20, 30, 40, 50}
    mySlice := myArray[1:4] // [20 30 40] (length 3, capacity 4)
    
    • myArray[start:end] creates a slice referencing a portion of the array.
    • start is inclusive, end is exclusive.
    • If start is omitted, it defaults to 0.
    • If end is omitted, it defaults to the length of the array.

Slice Operations

  • Accessing Elements: Use indexing like arrays.

    mySlice := []int{1, 2, 3}
    firstElement := mySlice[0] // firstElement is 1
    
  • Modifying Elements:

    mySlice := []int{1, 2, 3}
    mySlice[1] = 10 // mySlice is now [1 10 3]
    
  • append(): Adds elements to the end of a slice. If the capacity is reached, a new underlying array is allocated, and the elements are copied.

    mySlice := []int{1, 2, 3}
    mySlice = append(mySlice, 4, 5) // mySlice is now [1 2 3 4 5]
    
  • copy(): Copies elements from one slice to another.

    src := []int{1, 2, 3}
    dst := make([]int, len(src))
    copy(dst, src) // dst is now [1 2 3]
    
  • len() and cap(): Return the length and capacity of a slice, respectively.

    mySlice := []int{1, 2, 3, 4, 5}
    length := len(mySlice) // length is 5
    capacity := cap(mySlice) // capacity is 5
    

Slice Examples

package main

import "fmt"

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

	// Append elements
	numbers = append(numbers, 4, 5)
	fmt.Println("After append:", numbers) // Output: After append: [1 2 3 4 5]

	// Slice a portion of the slice
	subSlice := numbers[1:4]
	fmt.Println("Subslice:", subSlice) // Output: Subslice: [2 3 4]

	// Modify the subslice (affects the original slice)
	subSlice[0] = 100
	fmt.Println("Modified subslice:", subSlice) // Output: Modified subslice: [100 3 4]
	fmt.Println("Original slice:", numbers) // Output: Original slice: [1 100 3 4 5]

	// Create a slice with make
	names := make([]string, 0, 3) // Length 0, Capacity 3
	names = append(names, "Alice")
	names = append(names, "Bob")
	names = append(names, "Charlie")
	fmt.Println("Names:", names) // Output: Names: [Alice Bob Charlie]
	fmt.Println("Length:", len(names)) // Output: Length: 3
	fmt.Println("Capacity:", cap(names)) // Output: Capacity: 3

	// Appending beyond capacity causes reallocation
	names = append(names, "David")
	fmt.Println("After appending David:", names) // Output: After appending David: [Alice Bob Charlie David]
	fmt.Println("New Capacity:", cap(names)) // Output: New Capacity: 6 (typically doubles)
}

Important Considerations

  • Zero Value: The zero value of a slice is nil. A nil slice has a length and capacity of 0 and no underlying array.

  • Slice Growth: When append() exceeds the capacity, a new underlying array is allocated, and the existing elements are copied. This can be expensive, especially for large slices. Consider pre-allocating capacity with make() if you know the approximate size of the slice beforehand.

  • Sharing Underlying Arrays: Be mindful that multiple slices can share the same underlying array. Modifying one slice can affect others. If you need independent copies, use copy() to create a new slice with its own underlying array.

  • range Loop: Slices can be easily iterated over using the range keyword.

    mySlice := []string{"apple", "banana", "cherry"}
    for index, value := range mySlice {
        fmt.Printf("Index: %d, Value: %s\n", index, value)
    }
    

Slices are a powerful and versatile data structure in Go. Understanding their behavior, especially regarding length, capacity, and underlying arrays, is crucial for writing efficient and correct Go programs.