Slices
What Is a Slice?β
Slices are the most widely-used data structure in Go. Unlike fixed-length arrays, a slice is a dynamically-sized sequence whose length can grow at runtime.
Internally a slice is a three-field header:
Slice header (24 bytes on 64-bit)
ββββββββββββ¬βββββββββ¬ββββββββββ
β ptr β len β cap β
β (8 byte) β(8 byte)β (8 byte)β
ββββββββββββ΄βββββββββ΄ββββββββββ
β
βΌ
ββββββ¬βββββ¬βββββ¬βββββ¬βββββ
β 0 β 1 β 2 β 3 β 4 β β backing array (heap-allocated)
ββββββ΄βββββ΄βββββ΄βββββ΄βββββ
- ptrβ pointer to the first element of the backing array
- lenβ number of elements currently in use
- capβ total capacity of the backing array (max elements before reallocation)
Declaring and Initialising Slicesβ
package main
import "fmt"
func main() {
// 1. nil slice (len=0, cap=0, ptr=nil)
var s1 []int
fmt.Println(s1, len(s1), cap(s1)) // [] 0 0
fmt.Println(s1 == nil) // true
// 2. Literal initialisation
s2 := []int{1, 2, 3, 4, 5}
fmt.Println(s2, len(s2), cap(s2)) // [1 2 3 4 5] 5 5
// 3. make(type, len, cap) β cap defaults to len when omitted
s3 := make([]int, 3, 10)
fmt.Println(s3, len(s3), cap(s3)) // [0 0 0] 3 10
// 4. Slicing an array
arr := [5]int{10, 20, 30, 40, 50}
s4 := arr[1:4] // indices 1..3 (4 excluded)
fmt.Println(s4, len(s4), cap(s4)) // [20 30 40] 3 4
}
Slice Expressionsβ
The full form is s[low:high:max].
package main
import "fmt"
func main() {
s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
// s[low:high] β len=high-low, cap=len(s)-low
a := s[2:5]
fmt.Printf("a=%v len=%d cap=%d\n", a, len(a), cap(a))
// a=[2 3 4] len=3 cap=8
// s[low:high:max] β cap=max-low (three-index slice)
b := s[2:5:6]
fmt.Printf("b=%v len=%d cap=%d\n", b, len(b), cap(b))
// b=[2 3 4] len=3 cap=4
// Shorthand forms
fmt.Println(s[:3]) // [0 1 2] β same as s[0:3]
fmt.Println(s[7:]) // [7 8 9] β same as s[7:len(s)]
fmt.Println(s[:]) // [0 1 2 ... 9] β same as s[0:len(s)]
}
append and Automatic Reallocationβ
append adds elements to a slice. When cap is exceeded, Go allocates a new backing array and copies existing data.
package main
import "fmt"
func main() {
s := make([]int, 0, 4)
fmt.Printf("initial: %v len=%d cap=%d ptr=%p\n", s, len(s), cap(s), s)
for i := 1; i <= 8; i++ {
prevCap := cap(s)
prevPtr := fmt.Sprintf("%p", s)
s = append(s, i)
if cap(s) != prevCap {
fmt.Printf("reallocated! cap %dβ%d, ptr %sβ%p\n", prevCap, cap(s), prevPtr, s)
}
}
fmt.Println(s)
// Appending another slice with ...
a := []int{1, 2, 3}
b := []int{4, 5, 6}
a = append(a, b...)
fmt.Println(a) // [1 2 3 4 5 6]
}
copy β Creating an Independent Copyβ
copy(dst, src) copies min(len(dst), len(src)) elements and returns the count.
package main
import "fmt"
func main() {
src := []int{1, 2, 3, 4, 5}
// Simple assignment shares the backing array
shared := src
shared[0] = 999
fmt.Println(src) // [999 2 3 4 5] β original changed!
// copy creates an independent slice
src2 := []int{1, 2, 3, 4, 5}
dst := make([]int, len(src2))
n := copy(dst, src2)
dst[0] = 999
fmt.Println(src2) // [1 2 3 4 5] β original preserved
fmt.Println(dst) // [999 2 3 4 5]
fmt.Println(n) // 5 (elements copied)
// Partial copy
dst2 := make([]int, 3)
copy(dst2, src2[1:])
fmt.Println(dst2) // [2 3 4]
}
Shared Backing Array Pitfallsβ
Simple assignment or slicing shares the backing array. Unintended mutations can be subtle.
package main
import "fmt"
func main() {
base := []int{1, 2, 3, 4, 5}
// Slicing shares the backing array
sub := base[1:4] // [2 3 4]
sub[0] = 200
fmt.Println(base) // [1 200 3 4 5] β base changed too!
// append beyond cap creates a new backing array
base2 := []int{1, 2, 3, 4, 5}
sub2 := base2[1:3:3] // len=2, cap=2 (cap limited by third index)
sub2 = append(sub2, 99) // cap exceeded β new array allocated
sub2[0] = 200
fmt.Println(base2) // [1 2 3 4 5] β separated, no effect
fmt.Println(sub2) // [200 3 99]
}
Common Slice Patternsβ
package main
import "fmt"
func main() {
// Stack
var stack []int
stack = append(stack, 1, 2, 3) // push
top := stack[len(stack)-1] // peek
stack = stack[:len(stack)-1] // pop
fmt.Println(top, stack) // 3 [1 2]
// Queue
var queue []int
queue = append(queue, 1, 2, 3) // enqueue
front := queue[0] // peek front
queue = queue[1:] // dequeue
fmt.Println(front, queue) // 1 [2 3]
// Filter β keep even numbers
nums := []int{1, 2, 3, 4, 5, 6, 7, 8}
var evens []int
for _, n := range nums {
if n%2 == 0 {
evens = append(evens, n)
}
}
fmt.Println(evens) // [2 4 6 8]
// Delete element at index i (order preserved)
s := []int{1, 2, 3, 4, 5}
i := 2
s = append(s[:i], s[i+1:]...)
fmt.Println(s) // [1 2 4 5]
// Delete element (order not preserved, faster)
s2 := []int{1, 2, 3, 4, 5}
s2[i] = s2[len(s2)-1]
s2 = s2[:len(s2)-1]
fmt.Println(s2) // [1 2 5 4]
}
2-D Slicesβ
package main
import "fmt"
func main() {
rows := 3
matrix := make([][]int, rows)
for i := range matrix {
matrix[i] = make([]int, i+1) // triangular shape
for j := range matrix[i] {
matrix[i][j] = (i+1) * (j+1)
}
}
for _, row := range matrix {
fmt.Println(row)
}
// [1]
// [2 4]
// [3 6 9]
}
Practical Example β Sliding Window Maximumβ
package main
import "fmt"
// Returns the maximum in each sliding window of size k.
func maxSlidingWindow(nums []int, k int) []int {
if len(nums) == 0 || k == 0 {
return nil
}
result := make([]int, 0, len(nums)-k+1)
for i := 0; i <= len(nums)-k; i++ {
window := nums[i : i+k]
maxVal := window[0]
for _, v := range window[1:] {
if v > maxVal {
maxVal = v
}
}
result = append(result, maxVal)
}
return result
}
func main() {
nums := []int{1, 3, -1, -3, 5, 3, 6, 7}
fmt.Println(maxSlidingWindow(nums, 3))
// [3 3 5 5 6 7]
}