Slices: the basics
The previous lesson showed that arrays have a fixed length baked into their type. That rigidity makes them awkward for everyday work: you cannot hold a variable-length list of results, you cannot grow a collection as you read more input, and you cannot write a function that accepts "an array of any length".
Go's everyday solution is the slice. A slice wraps an underlying array but exposes a friendlier interface: variable length, one type for any length, and cheap to pass around. Slices are what other languages call a list, a resizable array, or a vector, and they carry almost every collection of values in real Go code.
Declaring a slice
The shortest way to make a slice is a literal with square brackets in front of the element type:
nums := []int{10, 20, 30}
fmt.Println(nums) // [10 20 30]
fmt.Println(len(nums)) // 3
No size inside the brackets, unlike an array. The compiler counts the elements and the slice's length is tracked at runtime rather than in the type.
The zero value of a slice is nil. A nil slice has length zero and no backing data:
var empty []int
fmt.Println(empty) // []
fmt.Println(len(empty)) // 0
fmt.Println(empty == nil) // true
You can read from a nil slice (len(empty) works) and range over it (the loop body simply never runs). What you cannot do is index into it; there is no element at position zero for the runtime to fetch:
var empty []int
fmt.Println(empty[0])
// runtime panic: index out of range [0] with length 0
A panic is a runtime crash. It means the program reached an invalid state that Go refuses to continue from, such as indexing past the end of a slice. When a panic happens, Go prints an error message and stops the current program.
You'll see panic and recover properly in the Errors chapter. For now, the important thing to know is that out-of-range indexing crashes the program instead of quietly returning a default value.
The next lesson covers make, the other common way to create a slice with room in it from the start.
Indexing and iterating
A slice is indexed with square brackets, starting at 0, the same syntax arrays use:
nums := []int{10, 20, 30}
fmt.Println(nums[0]) // 10
fmt.Println(nums[2]) // 30
nums[1] = 200
fmt.Println(nums) // [10 200 30]
The range form from Chapter 2 works exactly as it does on arrays, yielding index-value pairs:
for i, v := range nums {
fmt.Println(i, v)
}
Out-of-range indexing is a runtime panic: reading nums[5] on a three-element slice crashes the program. There is no silent wraparound or nil return. Guard with len(nums) if the index might be out of bounds.
Slices share their data
Here is the biggest behavioural difference from arrays. When you assign a slice to another variable, both variables end up pointing at the same underlying data. Changes through one are visible through the other:
a := []int{1, 2, 3}
b := a
b[0] = 99
fmt.Println(a) // [99 2 3] (changed)
fmt.Println(b) // [99 2 3]
Compare that with the array example from the previous lesson, where b := a copied every element. For slices, b := a does not make a second copy of the elements. Both variables still view the same backing array.
That is why changing b[0] changed a[0] too. It also helps explain why slices are cheap to pass to functions and why they are the default collection in Go. It is also the source of the aliasing bug covered in a later lesson.
One more thing before the next lesson
The two facts that matter most for now are:
- A slice has a length you can query with
len. - Assigning a slice does not copy its elements, so changing one slice can affect the other.
The next lesson introduces make, len, and cap together so you can see how slices are represented and how they grow.
The starter declares nums := []int{5, 10, 15, 20, 25}. Do three things in main:
- Print the length of
numson its own line. - Range over
numsand print only the values (use_for the index) on their own lines. - Assign
100tonums[0]. Then createalias := nums, assign999toalias[4], and printnums. You should see the aliasing rule in action:[100 10 15 20 999].
5 5 10 15 20 25 [100 10 15 20 999]