Golang Tutorial - Slices
Slices are one of the most fundamental and frequently used data structures in Go. Unlike arrays, which have a fixed size defined at compile time, slices are dynamic wrappers over arrays that can grow and shrink automatically as elements are added or removed.
1. Why Do We Need Slices? (The Problem with Arrays)
In Go, arrays have a rigid limitation: their size is part of their type and must be declared at compile time. This means you cannot resize an array once it is created.
If you do not know in advance how many elements your collection will hold, using a raw array becomes highly restrictive. Slices solve this problem by acting as dynamic arrays. They manage memory allocation automatically under the hood, making them the most widely used collection construct in Go development.
2. Core Mental Model: What is a Slice?
A slice does not store any data itself. Instead, it acts as a descriptor or a header pointing to an underlying array.
A slice header consists of three components:
- Pointer: A reference to the first element of the underlying array that the slice accesses.
- Length (
len): The number of elements currently present in the slice. - Capacity (
cap): The maximum number of elements the slice can hold before it must allocate a new underlying array.
Slice Memory Layout
3. Declaring and Initializing Slices
There are multiple ways to declare and initialize a slice in Go. Understanding how they differ in memory is crucial for writing bug-free code.
The Uninitialized (Nil) Slice
If you declare a slice without initializing it, its default value is nil (equivalent to null in other programming languages).
package main
import "fmt"
func main() {
var nums []int // Declared but not initialized
fmt.Println(nums) // Output: []
fmt.Println(nums == nil) // Output: true
fmt.Println(len(nums)) // Output: 0
}
The Slice Literal
You can declare and initialize a slice with values directly using a slice literal. This automatically allocates an underlying array with a size equal to the number of elements provided.
nums := []int{1, 2, 3}
fmt.Println(nums) // Output: [1 2 3]
fmt.Println(nums == nil) // Output: false
4. The make Function: Managing Length and Capacity
Go provides an built-in make function to allocate slices with pre-configured length and capacity. This is highly optimized for performance because it pre-allocates memory, avoiding frequent re-allocations.
Syntax
make([]Type, length, capacity)
Note: If you omit the capacity argument, it defaults to the same value as the length.
Example: Initializing with Length and Capacity
package main
import "fmt"
func main() {
// Length: 2, Capacity: 5
nums := make([]int, 2, 5)
fmt.Println(nums) // Output: [0 0] (Initialized with zero-values)
fmt.Println(nums == nil) // Output: false
fmt.Println(len(nums)) // Output: 2
fmt.Println(cap(nums)) // Output: 5
}
Note: Because the initial length was set to
2, Go automatically filled those first two slots with the zero-value of the slice's type (0forint,falseforbool,""forstring).
5. Appending Elements & Dynamic Resizing Behavior
To add elements to a slice, use Go's built-in append function.
Syntax Pitfall
The append function returns a updated slice header. You must assign this returned value back to your slice variable:
// Correct usage
nums = append(nums, 10)
How Dynamic Resizing Works (Capacity Doubling)
When you append elements to a slice and exceed its current capacity, Go performs the following steps behind the scenes:
- Allocates a new underlying array with a larger capacity (typically doubling the previous capacity).
- Copies the existing elements from the old array to the new array.
- Appends the new element.
- Updates the slice header to point to the new array.
Code Walkthrough: Observing Capacity Changes
package main
import "fmt"
func main() {
// Best Practice: Initialize with length 0 if you plan to append dynamically
nums := make([]int, 0, 5)
fmt.Printf("Initial -> Len: %d, Cap: %d, Vals: %v\n", len(nums), cap(nums), nums)
// Append elements within initial capacity
nums = append(nums, 1)
nums = append(nums, 2)
nums = append(nums, 3)
nums = append(nums, 4)
fmt.Printf("After 4 Appends -> Len: %d, Cap: %d, Vals: %v\n", len(nums), cap(nums), nums)
// Exceeding capacity (5 elements)
nums = append(nums, 5)
nums = append(nums, 6) // This triggers a resize!
// The capacity doubles from 5 to 10
fmt.Printf("After Exceeding Cap -> Len: %d, Cap: %d, Vals: %v\n", len(nums), cap(nums), nums)
}
Output:
6. Modifying Slices via Indexing (And Common Pitfalls)
You can access and modify slice elements using standard bracket notation slice[index]. However, you must be careful about the slice's current length.
The "Index Out of Range" Panic
You can only write directly to an index if it is within the current length of the slice. Writing to an index that is within the capacity but outside the length will cause a runtime panic.
Naive Approach (Fails)
nums := make([]int, 0, 5) // len = 0, cap = 5
nums[0] = 3 // PANIC: runtime error: index out of range [0] with length 0
Better Approach (Succeeds)
nums := make([]int, 2, 5) // len = 2, cap = 5
nums[0] = 3 // Works!
nums[1] = 5 // Works!
fmt.Println(nums) // Output: [3 5]
7. Copying Slices with the copy Function
Go provides a built-in copy function to copy elements from a source slice to a destination slice.
The Destination Length Pitfall
The copy function only copies elements up to the minimum length of the two slices: min(len(dest), len(src)). If your destination slice has a length of 0, no elements will be copied.
Naive Approach (Fails to Copy)
src := []int{1, 2, 3}
var dest []int // len = 0, cap = 0
copy(dest, src)
fmt.Println(dest) // Output: [] (Nothing was copied!)
Correct Approach (Succeeds)
package main
import "fmt"
func main() {
src := []int{1, 2, 3}
// Allocate a destination slice with the exact length of the source
dest := make([]int, len(src))
copy(dest, src)
fmt.Println("Source:", src) // Output: [1 2 3]
fmt.Println("Destination:", dest) // Output: [1 2 3]
}
8. The Slice Operator (Slicing Syntax)
The slice operator allows you to create a new sub-slice from an existing slice or array using the syntax slice[start:end].
- The
startindex is inclusive. - The
endindex is exclusive (elements up toend-1are taken).
Slicing Syntax Variations
Code Example
package main
import "fmt"
func main() {
nums := []int{1, 2, 3, 4, 5}
sub1 := nums[1:4] // Indices 1, 2, and 3
fmt.Println(sub1) // Output: [2 3 4]
sub2 := nums[:3] // Indices 0, 1, and 2
fmt.Println(sub2) // Output: [1 2 3]
sub3 := nums[2:] // Indices 2 to the end
fmt.Println(sub3) // Output: [3 4 5]
}
9. Comparing Slices using the slices Package
In Go, you cannot compare two slices directly using the == operator (e.g., nums1 == nums2 will result in a compilation error). Slices can only be compared directly to nil.
To compare the contents of two slices, you must use the standard library's slices package.
package main
import (
"fmt"
"slices" // Standard library package
)
func main() {
nums1 := []int{1, 2, 3}
nums2 := []int{1, 2, 3}
nums3 := []int{1, 2, 4}
// Compare equal slices
fmt.Println(slices.Equal(nums1, nums2)) // Output: true
// Compare unequal slices
fmt.Println(slices.Equal(nums1, nums3)) // Output: false
}
10. Multi-Dimensional (2D) Slices
Just like arrays, you can nest slices to create multi-dimensional structures.
package main
import "fmt"
func main() {
// Declaring a 2D slice
matrix := [][]int{
{1, 2, 3},
{4, 5, 6},
}
fmt.Println(matrix) // Output: [[1 2 3] [4 5 6]]
fmt.Println(matrix[0][1]) // Accessing element: Output: 2
}
Summary and Key Takeaways
- Dynamic Nature: Slices are dynamic wrappers over fixed-size arrays and are the most common collection type in Go.
- Nil Slices: An uninitialized slice is
nil, has a length of0, and does not point to an underlying array. - The
makeFunction: Usemake([]Type, len, cap)to pre-allocate memory for better performance. - Length vs. Capacity: Length (
len) is the number of elements in the slice; Capacity (cap) is the size of the underlying array. - Dynamic Resizing: When
appendexceeds capacity, Go automatically doubles the capacity of the underlying array. - Appending Best Practice: Always assign the result of
appendback to the slice variable (nums = append(nums, val)). - Copying Rule: The
copy(dest, src)function requires the destination slice to have an allocated length (len) greater than0to copy elements. - Slicing: The slice operator
[start:end]is inclusive ofstartand exclusive ofend. - Comparison: Slices cannot be compared with
==(except againstnil). Useslices.Equal()from the standard library to compare their contents.