Go Slices: A Window into an Array
Think of a Go slice not as a list, but as a lightweight window into an underlying array. It's used everywhere for managing sequences of data. The footgun: since slices can share memory, modifying one can unexpectedly alter another.
WHY IT EXISTS Go's arrays are rigid: their size is fixed and part of their type. Passing an array to a function copies the entire array, which is inefficient for large collections. Slices were created to provide a more flexible, powerful, and efficient way to work with sequences of data.
THE MENTAL MODEL A slice is not the data itself; it's a small descriptor or header for a contiguous segment of an array. Think of this header as a struct with three fields: a pointer to the underlying array, a length (the number of elements the slice contains), and a capacity (the number of elements from the start of the slice to the end of the underlying array). Passing a slice just copies this small header, not the data it points to.
HOW IT WORKS When you create a slice with make([]T, len, cap), Go allocates a new array and returns a slice that points to it. The key behavior is in "slicing" an existing array or slice, like new_slice := old_slice[1:5]. This creates a new slice header that points to the same underlying array as the original. It does not create a new copy of the data. This shared memory is efficient but is also the source of common bugs. If you modify an element in new_slice, the change will be visible in old_slice because they are both just windows looking at the same memory.
WHEN TO USE IT Use slices as the default for any sequence of elements in Go. They are the idiomatic choice for function parameters, return values, and any collection that might need to change in size. They are fundamental to building most Go programs, from reading file contents to processing API requests.
WHEN NOT TO USE IT Use a raw array only when you need a truly fixed-size collection and its size is known at compile time. An array guarantees its size and that it will be passed by value (copied). This can be a feature if you need to ensure a function cannot modify the original collection, but it's often an inefficient way to achieve immutability.
ONE CANONICAL EXAMPLE Creating a slice with make shows its core properties. s := make([]byte, 5, 10) creates a slice s with a length of 5 and a capacity of 10. It points to a newly allocated array of 10 bytes. If you then create t := s[2:4], the new slice t has a length of 2. However, its capacity is 8 (the original 10 minus the 2 elements we skipped at the beginning), because it can still "see" the rest of the original underlying array.
Read the original → go.dev
Get five bites like this every day.
Tezvyn delivers a daily feed of 60-second tech bites with quizzes to lock in what you learn.