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
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:
The appropriate choice depends on the specific requirements of your application.
Pros
Cons
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.