Introduction
Slices are Go’s primary dynamic array type. Unlike arrays, slices can grow and shrink. The append function is the standard way to add elements to a slice. Understanding how slices and append work internally helps you write efficient Go code and avoid subtle bugs. See Go Installation Guide, Go Ecosystem Overview, Go Best Practices for more context.
Slice Internals
A slice is a three-field struct under the hood:
type slice struct {
array unsafe.Pointer // pointer to underlying array
len int // number of elements
cap int // capacity of underlying array
}
// Create a slice
s := []int{1, 2, 3}
fmt.Println(len(s), cap(s)) // => 3 3
// Create with make (length, capacity)
s2 := make([]int, 3, 10)
fmt.Println(len(s2), cap(s2)) // => 3 10
// Slice of a slice shares the underlying array
a := []int{1, 2, 3, 4, 5}
b := a[1:3] // [2, 3]
fmt.Println(len(b), cap(b)) // => 2 4 (cap from index 1 to end of a)
The append Function
append adds elements to the end of a slice and returns the updated slice:
// Append single elements
x := []int{1, 2, 3}
x = append(x, 4)
x = append(x, 5, 6, 7)
fmt.Println(x) // => [1 2 3 4 5 6 7]
// Always assign the result back — append may return a new slice
x = append(x, 8) // correct
append(x, 9) // wrong — result discarded
Appending a Slice to a Slice
Use the ... spread operator to append all elements of one slice to another:
x := []int{1, 2, 3}
y := []int{4, 5, 6}
// Append all elements of y to x
x = append(x, y...)
fmt.Println(x) // => [1 2 3 4 5 6]
// Without ..., it won't compile:
// x = append(x, y) // ERROR: cannot use y (type []int) as type int
How append Handles Capacity
When the slice has enough capacity, append adds elements in place. When capacity is exceeded, Go allocates a new, larger array:
s := make([]int, 0, 3) // length 0, capacity 3
fmt.Println(len(s), cap(s)) // => 0 3
s = append(s, 1, 2, 3)
fmt.Println(len(s), cap(s)) // => 3 3 (fits in existing capacity)
s = append(s, 4) // exceeds capacity — new array allocated
fmt.Println(len(s), cap(s)) // => 4 6 (capacity doubled)
s = append(s, 5, 6)
fmt.Println(len(s), cap(s)) // => 6 6
s = append(s, 7) // exceeds capacity again
fmt.Println(len(s), cap(s)) // => 7 12 (doubled again)
Capacity growth strategy: Go typically doubles capacity for small slices and grows by ~1.25x for larger ones. The exact strategy varies by Go version.
The Sharing Problem
When two slices share the same underlying array, modifying one can affect the other:
a := []int{1, 2, 3, 4, 5}
b := a[:3] // b shares a's array
b[0] = 99
fmt.Println(a) // => [99 2 3 4 5] — a was modified!
fmt.Println(b) // => [99 2 3]
// append within capacity also modifies the shared array
b = append(b, 100)
fmt.Println(a) // => [99 2 3 100 5] — a[3] was overwritten!
Fix: Use copy or a full slice expression to prevent sharing:
// Option 1: copy to a new slice
b := make([]int, 3)
copy(b, a[:3])
// Option 2: three-index slice (limits capacity, forces new array on append)
b := a[:3:3] // len=3, cap=3 — append will allocate new array
b = append(b, 100)
fmt.Println(a) // => [1 2 3 4 5] — a is unchanged
Common Patterns
Building a Slice Incrementally
// Pre-allocate when you know the size
result := make([]int, 0, len(input))
for _, v := range input {
if v > 0 {
result = append(result, v)
}
}
Removing an Element
// Remove element at index i (order preserved)
func remove(s []int, i int) []int {
return append(s[:i], s[i+1:]...)
}
// Remove element at index i (order not preserved — faster)
func removeUnordered(s []int, i int) []int {
s[i] = s[len(s)-1]
return s[:len(s)-1]
}
s := []int{1, 2, 3, 4, 5}
s = remove(s, 2)
fmt.Println(s) // => [1 2 4 5]
Inserting an Element
func insert(s []int, i int, val int) []int {
s = append(s, 0) // grow by 1
copy(s[i+1:], s[i:]) // shift elements right
s[i] = val
return s
}
s := []int{1, 2, 4, 5}
s = insert(s, 2, 3)
fmt.Println(s) // => [1 2 3 4 5]
Deduplication
func unique(s []int) []int {
seen := make(map[int]bool)
result := s[:0] // reuse the same underlying array
for _, v := range s {
if !seen[v] {
seen[v] = true
result = append(result, v)
}
}
return result
}
s := []int{1, 2, 2, 3, 3, 3, 4}
fmt.Println(unique(s)) // => [1 2 3 4]
Filter In-Place
// Filter without allocating a new slice
func filter(s []int, keep func(int) bool) []int {
n := 0
for _, v := range s {
if keep(v) {
s[n] = v
n++
}
}
return s[:n]
}
s := []int{1, -2, 3, -4, 5}
s = filter(s, func(v int) bool { return v > 0 })
fmt.Println(s) // => [1 3 5]
Stack (LIFO)
// Push
stack = append(stack, value)
// Pop
value, stack = stack[len(stack)-1], stack[:len(stack)-1]
// Peek
top := stack[len(stack)-1]
Queue (FIFO)
// Enqueue
queue = append(queue, value)
// Dequeue
value, queue = queue[0], queue[1:]
Performance Tips
Pre-allocate When Size is Known
// Slow: many reallocations
result := []int{}
for i := 0; i < 10000; i++ {
result = append(result, i)
}
// Fast: single allocation
result := make([]int, 0, 10000)
for i := 0; i < 10000; i++ {
result = append(result, i)
}
Avoid Unnecessary Copies
// Passing a slice to a function doesn't copy the data
func process(s []int) {
// s is a slice header (24 bytes), not a copy of the data
for i := range s {
s[i] *= 2 // modifies the original
}
}
// To prevent modification, pass a copy
func processSafe(s []int) {
s = append([]int{}, s...) // copy
for i := range s {
s[i] *= 2
}
}
Benchmark: append vs pre-allocated
func BenchmarkAppend(b *testing.B) {
for i := 0; i < b.N; i++ {
s := []int{}
for j := 0; j < 1000; j++ {
s = append(s, j)
}
}
}
func BenchmarkPrealloc(b *testing.B) {
for i := 0; i < b.N; i++ {
s := make([]int, 0, 1000)
for j := 0; j < 1000; j++ {
s = append(s, j)
}
}
}
// BenchmarkPrealloc is typically 2-3x faster
Nil Slices vs Empty Slices
var nilSlice []int // nil slice
emptySlice := []int{} // empty slice (non-nil)
fmt.Println(nilSlice == nil) // => true
fmt.Println(emptySlice == nil) // => false
fmt.Println(len(nilSlice)) // => 0
fmt.Println(len(emptySlice)) // => 0
// append works on nil slices
nilSlice = append(nilSlice, 1, 2, 3)
fmt.Println(nilSlice) // => [1 2 3]
// JSON encoding difference
json.Marshal(nilSlice) // => null
json.Marshal(emptySlice) // => []
Summary
| Operation | Code |
|---|---|
| Append element | s = append(s, v) |
| Append slice | s = append(s, other...) |
| Pre-allocate | s = make([]int, 0, n) |
| Copy | copy(dst, src) |
| Remove at i | append(s[:i], s[i+1:]...) |
| Insert at i | append(s[:i], append([]T{v}, s[i:]...)...) |
| Stack push | stack = append(stack, v) |
| Stack pop | v, stack = stack[len-1], stack[:len-1] |
Comments