Skip to main content

Go Slices: Dynamic Arrays and Operations

Published: December 17, 2025 Updated: August 29, 2026 Larry Qu 5 min read

Slices are Go’s primary sequence type — dynamic, flexible, and built on top of arrays. A slice is a three-word header: a pointer to an underlying array, a length (how many elements are accessible), and a capacity (how many elements the underlying array can hold before reallocation). Most of the slice behavior that surprises developers comes from understanding this header.

For arrays (the fixed-size foundation) see Go arrays creation and indexing.

The Slice Header

s := make([]int, 3, 5)
// s = {ptr: 0x..., len: 3, cap: 5}
//       ↑              ↑           ↑
//       points to      accessible  total array
//       [0 0 0 0 0]    elements    capacity

len(s) is what you can access. cap(s) is how much room exists before a new array must be allocated. When you append beyond capacity, Go allocates a larger array, copies, and updates the slice header.

Creating Slices

// Slice literal — len=3, cap=3
s1 := []int{1, 2, 3}

// make(type, len, cap) — cap optional, defaults to len
s2 := make([]int, 5)      // len=5, cap=5
s3 := make([]int, 0, 10)  // len=0, cap=10 — pre-allocated, empty

// Nil slice — the zero value; len=0, cap=0
var s4 []int  // nil, but safe to append to
fmt.Println(s4 == nil)    // true
fmt.Println(len(s4))      // 0
s4 = append(s4, 1)       // works fine

make([]int, 0, 10) pre-allocates a backing array of 10 elements. No reallocation happens until you append beyond 10. Use this when you know the approximate final size to avoid repeated reallocations.

append: Growth Behavior

append adds elements, returning a new slice header (which may point to a new array if the old one was full):

s := []int{1, 2, 3}
s = append(s, 4)           // single element
s = append(s, 5, 6, 7)    // multiple elements
other := []int{8, 9}
s = append(s, other...)   // append another slice

fmt.Println(s)  // [1 2 3 4 5 6 7 8 9]

When capacity is exceeded, Go allocates a new array — currently roughly doubling up to 1024 elements, then growing by ~25%. The old array becomes eligible for GC.

Always reassign s = append(s, ...)append may return a different slice header if reallocation occurred.

The Sharing Trap

Slices derived from the same array share memory. Modifying one can silently affect the other:

original := []int{1, 2, 3, 4, 5}
sub := original[1:4]  // [2 3 4] — shares original's array

sub[0] = 20
fmt.Println(original)  // [1 20 3 4 5] — original modified!

This also affects append when capacity allows:

a := make([]int, 3, 5)  // [0 0 0], cap=5
b := a[1:3]             // [0 0], shares a's array, cap=4

b = append(b, 99)       // fits in a's capacity — overwrites a[3]!
fmt.Println(a)           // [0 0 0 99 0] — a was modified through b

Fix: use a three-index slice to restrict capacity, forcing append to allocate independently:

b := a[1:3:3]           // len=2, cap=2 (not 4)
b = append(b, 99)       // cap exceeded — new array allocated
fmt.Println(a)           // [0 0 0 0 0] — a is unchanged

copy: Making Independent Copies

copy(dst, src) copies min(len(dst), len(src)) elements and returns the count:

original := []int{1, 2, 3, 4, 5}

// Full copy
clone := make([]int, len(original))
copy(clone, original)
clone[0] = 99
fmt.Println(original)  // [1 2 3 4 5] — unchanged

// Partial copy
first3 := make([]int, 3)
copy(first3, original)
fmt.Println(first3)  // [1 2 3]

// Copy within a slice (overlapping is handled correctly)
copy(original[1:], original[2:])  // shift left by one
fmt.Println(original)  // [1 3 4 5 5]

copy is the correct way to get an independent slice — not b := a (that’s a header copy sharing the same array).

Common Slice Tricks

Remove element at index i (order preserved):

s = append(s[:i], s[i+1:]...)

Remove element at index i (fast, doesn’t preserve order):

s[i] = s[len(s)-1]
s = s[:len(s)-1]

Insert at index i:

s = append(s, 0)                  // grow by one
copy(s[i+1:], s[i:])              // shift right
s[i] = newVal

Reverse in place:

for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
    s[i], s[j] = s[j], s[i]
}

Deduplicate (requires sorted slice):

j := 0
for i := 1; i < len(s); i++ {
    if s[i] != s[j] {
        j++
        s[j] = s[i]
    }
}
s = s[:j+1]

Slice of Structs vs Slice of Pointers

A common design question: []User or []*User?

// []User: values stored contiguously — better cache locality, copies on append
users := []User{{Name: "Alice"}, {Name: "Bob"}}
users[0].Name = "Alicia"  // direct modification

// []*User: pointers stored contiguously — stable addresses, heap allocation per user
ptrs := []*User{{Name: "Alice"}, {Name: "Bob"}}
ptrs[0].Name = "Alicia"  // same modification

Prefer []T for small structs — the contiguous memory is more cache-friendly. Use []*T when:

  • You need stable pointers (a pointer to users[0] becomes invalid after append reallocates users)
  • T is large and copying is expensive
  • nil needs to represent “absent” within the slice

Performance: Pre-allocate When Size Is Known

// ❌ Repeated allocations as slice grows — O(n) allocations
var result []int
for i := 0; i < n; i++ {
    result = append(result, expensive(i))
}

// ✅ One allocation — O(1) allocations
result := make([]int, 0, n)
for i := 0; i < n; i++ {
    result = append(result, expensive(i))
}

The difference matters at scale: appending 100k elements without pre-allocation causes ~17 reallocations (doubling from 1 to 131072). With make([]int, 0, 100000), it’s one allocation.

Summary

  • A slice is a pointer + len + cap; len is accessible elements, cap is room before reallocation
  • append returns a new header (possibly pointing to a new array); always reassign s = append(s, ...)
  • Sub-slices share the underlying array — modifying one affects the other; use three-index a[l:h:m] to control capacity
  • copy(dst, src) makes a true independent copy; simple assignment b := a only copies the header
  • Pre-allocate with make([]T, 0, n) when final size is known — avoids O(n) reallocations
  • Use []T for small structs (cache-friendly); []*T when stable addresses or nil elements are needed

Resources

Comments

👍 Was this article helpful?