Go Slice Operations Cheat Sheet
Slices are Go's dynamic array: a (pointer, length, capacity) header pointing into a backing array. This snippet covers the common operations: `append` and capacity growth, `copy` between slices, reslicing pitfalls (sharing the backing array), and a clean way to delete an element from the middle. Understand the header model and surprising slice aliasing bugs become obvious.
160 views
1
package main
import "fmt"
func main() {
var s []int // nil slice; len=0 cap=0
s = append(s, 1, 2, 3)
fmt.Printf("s=%v len=%d cap=%d\n", s, len(s), cap(s))
// append may reallocate; track capacity to see when.
for i := 4; i <= 8; i++ {
s = append(s, i)
fmt.Printf("i=%d cap=%d\n", i, cap(s))
}
// make([]T, len, cap) pre-sizes to avoid reallocation.
pre := make([]int, 0, 10)
pre = append(pre, 1, 2, 3)
fmt.Printf("pre len=%d cap=%d\n", len(pre), cap(pre))
}append(slice, vals...) returns a possibly-new slice header; if the backing array still has capacity it reuses it, otherwise it allocates a new one (typically doubling). Always reassign the result, since otherwise the new header is dropped on the floor. The capacity-vs-length distinction matters when chains of slices share the same backing array (next accordion). Pre-allocate with make([]T, 0, capacity) when you know roughly how many elements you will append; this is one of the easiest performance wins in Go.
package main
import "fmt"
func main() {
src := []int{1, 2, 3, 4, 5}
// copy(dst, src) copies min(len(dst), len(src)) elements; pre-size dst.
dst := make([]int, len(src))
n := copy(dst, src)
fmt.Printf("copied %d elements: %v\n", n, dst)
// Reslicing shares the backing array! Modifying view shows up in src.
view := src[1:3]
view[0] = 999
fmt.Printf("src after view edit: %v\n", src)
// To detach, copy into a fresh slice with full-slice expression.
detached := append([]int(nil), src[1:3]...)
detached[0] = -1
fmt.Printf("src untouched: %v detached: %v\n", src, detached)
}Go slices share the backing array of whatever they were derived from. view := src[1:3] creates a header pointing into src's array, so view[i] = x writes into src too. This is one of Go's most common surprises and the cause of many subtle bugs in libraries that hand out subslices to callers. To genuinely copy the elements, use copy(dst, src) into a freshly allocated slice or append([]T(nil), src...) for a one-liner that allocates and copies. The full-slice expression src[1:3:3] also limits the capacity, preventing later appends on the subslice from clobbering src.
package main
import "fmt"
// Order-preserving delete is O(n) because the tail must shift.
func deleteAt(s []int, i int) []int {
return append(s[:i], s[i+1:]...)
}
// O(1) swap-and-pop, but the order of remaining elements changes.
func swapDelete(s []int, i int) []int {
s[i] = s[len(s)-1]
return s[:len(s)-1]
}
func main() {
a := []int{10, 20, 30, 40, 50}
a = deleteAt(a, 2)
fmt.Printf("after deleteAt(2): %v\n", a) // [10 20 40 50]
b := []int{10, 20, 30, 40, 50}
b = swapDelete(b, 1)
fmt.Printf("after swapDelete(1): %v\n", b) // [10 50 30 40] (order changed)
}Slices have no built-in delete; you splice with append(s[:i], s[i+1:]...) to remove index i while preserving order. That copies the entire tail, so it is O(n). When order does not matter, swap-and-pop is O(1): move the last element into position i and truncate. Reach for swap-and-pop in hot paths over unordered collections (free-lists, contention queues, expired-entry pruning); use the order-preserving form whenever the callers see the slice. Both forms reslice the same backing array, so previous slice headers may now reference 'deleted' values that are still in memory; nil out elements containing pointers if you care about garbage collection.
