Go Module 4 – Arrays, slices, maps and strings
Slices: length, capacity, append and aliasing
How a Go slice points into an array, what len and cap count, when append writes into a shared array or copies to a new one, and how to keep two slices apart.
What you will learn
- Explain the three parts of a slice, a pointer, a length and a capacity, and read them with len and cap
- Predict when append writes into the shared array and when it copies to a new one
- Prevent slices from overwriting each other with full slice expressions, copy and slices.Clone
- Size slices in advance with make, and choose between a nil slice and an empty one
- Release the large array that a small slice keeps in memory
Before you start
What you’ll learn · Before you start
What you will learn
- Explain the three parts of a slice, a pointer, a length and a capacity, and read them with len and cap
- Predict when append writes into the shared array and when it copies to a new one
- Prevent slices from overwriting each other with full slice expressions, copy and slices.Clone
- Size slices in advance with make, and choose between a nil slice and an empty one
- Release the large array that a small slice keeps in memory
Before you start
On this page
Two appends to one slice can overwrite each other: when dirs holds 3 names with room for 4, append(dirs, "draft.txt") and append(dirs, "final.txt") both come back ending in final.txt. A slice: A view of part of an array, written []T: a pointer to its first element, a length and a capacity. Copying a slice copies only those three fields, so two slices can share elements. owns
no elements of its own; it describes part of an array, and append writes into that array whenever it has room.
You will read the three parts of a slice, predict when append shares an array and when it copies, and keep slices
apart with a full slice expression. All the output on this page was printed by the programs as they ran with Go
1.27.1.
A slice is a pointer, a length and a capacity
Slicing an array gives a view of some of its elements, and writing through the view writes the array:
// window.go: a slice describes part of an array with a pointer, a length and a capacity.
package main
import "fmt"
func main() {
arr := [6]int{10, 20, 30, 40, 50, 60}
s := arr[1:4] // elements 1, 2 and 3 of arr
fmt.Println(s, "len", len(s), "cap", cap(s))
s[0] = 99 // s has no elements of its own: this writes arr[1]
fmt.Println(arr)
t := s[:cap(s)] // a slice of s may reach as far as its capacity
fmt.Println(t, "len", len(t), "cap", cap(t))
} Output
[20 30 40] len 3 cap 5 [10 99 30 40 50 60] [99 30 40 50 60] len 5 cap 5
Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run window.go
arr[1:4] reached three elements, but cap said 5. The specification says a slice value holds a length, a capacity
and a reference to its underlying array, the backing array: The array that holds a slice's elements. Slices taken from one another share it, so a write through one slice is seen by every slice that reaches the same element.. The length counts the
elements the slice can index now; the capacity: The number of elements from a slice's first element to the end of its backing array, reported by cap. append writes into that room before it needs a new array. counts every element from its first one to
the end of the array. Step through the program and watch what each slice reaches:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- An array of six. arr is an array of six ints, at indexes 0 to 5. It is the only storage in this program; every slice that follows describes part of it.
- s := arr[1:4]. s is a slice header of three parts. Its pointer is the address of arr[1], its length is 3, the elements at indexes 1, 2 and 3, and its capacity is 5, every element from arr[1] to the end of the array.
- s[0] = 99. s[0] is the element the pointer names, arr[1]. The write lands in the array, so arr prints as [10 99 30 40 50 60].
- Up to the capacity. t := s[:cap(s)] keeps the same pointer and takes the length up to the capacity, 5, so t reaches arr[1] to arr[5] and prints [99 30 40 50 60].
- Nothing was copied. arr, s and t read and write the same six ints. Only the headers differ, each with its own start, length and capacity.
s[0] = 99 changed arr[1], because a slice has no elements to change except the array’s. And s[:cap(s)] reached
two elements that s itself could not index: a slice can be resliced up to its capacity, never past it, so s[:6]
would stop the program with the panic slice bounds out of range [:6] with capacity 5. Nothing in the program copied
an element; three variables used one array of six ints.
Checkpoint
Check: Pointer, length and capacity
append writes into the array while there is room, then copies
append(s, x) returns a new slice header. If the length of s plus the new elements still fits its capacity, the
specification says append re-uses the underlying array: it writes the new elements just past the length and returns
a longer view of the same array. Only when they do not fit does it allocate a new, larger array and copy the old
elements into it. That is the whole story of the two paths in the opening:
// two_appends.go: two appends to one slice with spare room, and the full slice expression that keeps them apart.
package main
import "fmt"
func main() {
dirs := make([]string, 0, 4)
dirs = append(dirs, "home", "asha", "notes") // length 3, room for 4
draft := append(dirs, "draft.txt")
final := append(dirs, "final.txt")
fmt.Println(draft)
fmt.Println(final)
safe := dirs[:3:3] // the same elements, capacity 3: no spare room
draft = append(safe, "draft.txt")
final = append(safe, "final.txt")
fmt.Println(draft)
fmt.Println(final)
fmt.Println("caps:", cap(dirs), cap(safe), cap(draft))
} Output
[home asha notes final.txt] [home asha notes final.txt] [home asha notes draft.txt] [home asha notes final.txt] caps: 4 3 6
Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run two_appends.go
Step through both halves of the run:
The drawing shows the whole scene; its 7 steps are listed below.
Every step as text
- Room for one more. dirs holds three names in an array with room for four, so its length is 3 and its capacity is 4.
- The first append. append(dirs, "draft.txt") finds room, writes draft.txt into element 3 of the same array, and returns a header of length 4.
- The second append. dirs still has length 3, so append(dirs, "final.txt") finds the same free element and writes final.txt over draft.txt.
- Both say final.txt. draft and final are two headers over one array, and element 3 holds final.txt, so both print [home asha notes final.txt].
- safe := dirs[:3:3]. A full slice expression keeps the three names but sets the capacity to 3, so safe has no free element left.
- append copies. append(safe, "draft.txt") finds no room, so it copies the three names into a new array, adds draft.txt there and returns a header of capacity 6.
- Each its own array. The second append copies again, into another array, so draft keeps draft.txt and final holds final.txt. Neither write touched the array of dirs.
dirs had length 3 and capacity 4, so both appends wrote into its element 3, and the second write replaced the first:
draft and final both printed final.txt. The fix, dirs[:3:3], is a
full slice expression: A slice expression with three indexes, s[low:high:max]. The third index sets the capacity of the result to max - low, so a later append cannot write past high into shared elements.: its third index sets the capacity to max - low, here
3, so nothing is left to share and each append copied into an array of its own. slices.Clip(dirs) returns the same
dirs[:len(dirs):len(dirs)].
Ignoring what append returns
append(s, x) on a line of its own would throw away the new header, and with it any new array, so the compiler
refuses it; for a slice of strings it reports append(s, x) (value of type []string) is not used. Assign the
result, normally to the same variable: s = append(s, x).
Assigning the result is also what keeps a slice growing in a loop: each append may hand back a header over a new array, and only the assignment carries it into the next pass.
Key idea
append writes into the backing array while the capacity allows, so two slices made from one base can overwrite each other; take away the spare capacity, or copy, before handing a base to two appends.
Checkpoint
Check: When append shares and when it copies
Sharing a backing array is what makes slicing cheap, and forgetting that two slices share one is what causes aliasing
bugs. Three more tools decide how slices share: make([]T, 0, n) makes room for n elements in advance, copy fills
an existing slice from another, and slices.Clone gives a slice an array of its own. A nil slice, the zero value of a
slice type, has no array at all until the first append makes one.
Growth, preallocation and nil slices, measured Pro: opens with a Pro, Premium or Ultimate pass
How much room append adds, and when to make that room yourself, have measured answers. This part records the capacities Go 1.27.1 chose, counts the allocations that preallocating saves, and shows where a nil slice and an empty one part ways.
In this part:
- The capacities append chose, recorded on Go 1.27.1
- make with a capacity: 16 allocations down to 1
- nil and empty slices: the same to len and append, different to JSON
4 runnable examples (Go) · about 5 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Aliasing failures reproduced, and the copies that fix them Pro: opens with a Pro, Premium or Ultimate pass
Shared arrays cause bugs that print no error: a copy that copies nothing, a removal that changes the caller’s data, rows that overwrite each other and a few bytes that keep 64 MiB in memory. Each one is reproduced here beside the fix that removes it.
In this part:
- copy moves as many elements as the shorter slice holds
- A removal that reuses the caller’s array changes the caller’s data
- Rows cut from one array overwrite each other
- Sixteen bytes sliced from a buffer kept 64 MiB in memory
4 runnable examples (Go) · about 5 minutes
Opening this part with your pass…Pro part. It opens with the same pass as the part above.
Practise
Practice covers the two ideas above, the parts of a slice and what append shares, beginning with the one your
earlier answers left weaker. A wrong choice tells you what it misses, and a right one why it holds:
Practice that adapts to you
Practise this lesson
6 questions about 4 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up (fresher): What does a slice value hold?
Three things: a pointer to an element of a backing array, a length and a capacity. The elements themselves are in
the array, so copying a slice copies only these three fields, and two slices can share the same elements. len
reports the length and cap the capacity.
Three longer questions of the module bank build on this lesson:
-
Mid to senior
A service reads 64 MiB log files whole, keeps the first 32 bytes of each as its header in a map and drops the rest. After 100 files the heap holds over 6 GiB. Explain why, say how you would confirm it, and fix it.
-
Fresher to mid
RemoveAt(s []int, i int) []int returns append(s[:i], s[i+1:]...). A caller removes index 2 from [1 2 3 4 5], prints its own slice and sees [1 2 4 5 5]. Explain the output, then design the function's API.
Key takeaways
- A slice is a pointer into a backing array with a length and a capacity; slicing copies no elements, and a write through any slice of the array changes the array.
- The capacity counts from the slice’s first element to the end of the array, and a slice can be resliced up to its capacity.
appendwrites into the backing array while the capacity allows and copies to a new array only when it does not, so two appends to one base can overwrite each other.- A full slice expression,
s[low:high:max], orslices.Clip, removes the spare capacity, so the next append copies.
Terms in this lesson
- backing array
- The array that holds a slice's elements. Slices taken from one another share it, so a write through one slice is seen by every slice that reaches the same element.
- capacity
- The number of elements from a slice's first element to the end of its backing array, reported by cap. append writes into that room before it needs a new array.
- full slice expression
- A slice expression with three indexes, s[low:high:max]. The third index sets the capacity of the result to max - low, so a later append cannot write past high into shared elements.
- slice
- A view of part of an array, written []T: a pointer to its first element, a length and a capacity. Copying a slice copies only those three fields, so two slices can share elements.
References
- The Go Programming Language Specification: Slice types (The Go Authors)
- The Go Programming Language Specification: Representation of values (The Go Authors)
- The Go Programming Language Specification: Slice expressions (The Go Authors)
- The Go Programming Language Specification: Appending and copying slices (The Go Authors)
- The Go Programming Language Specification: Making slices, maps and channels (The Go Authors)
- Go Slices: usage and internals (The Go Authors)
- Arrays, slices (and strings): The mechanics of append (The Go Authors)
- Package slices: Clip, Clone and Delete (The Go Authors)
- Go 1.25 Release Notes: Faster slices (The Go Authors)
- Go 1.26 Release Notes: Compiler (The Go Authors)
- Package testing: AllocsPerRun (The Go Authors)
- Go Code Review Comments: Declaring Empty Slices (The Go Authors)
- Package bytes: Fields (The Go Authors)
- Package bufio: Scanner.Bytes (The Go Authors)
- Source file runtime/slice.go: growslice and nextslicecap (The Go Authors)
- Package encoding/json: Marshal (The Go Authors)
- Package encoding/json/v2: Marshal and FormatNilSliceAsNull (The Go Authors)
Report a problem with this lesson
Kept only in this browser. Your Learn progress