Chapter 21: Go Slices
slices.
After we talked about arrays (fixed-size, rarely used directly), now we come to slices — the dynamic, flexible, everyday “list” type that almost everyone confuses with arrays at first… but once you understand the difference, slices become your best friend in 95%+ of list-related code.
Let’s go through everything about Go slices like we’re pair-programming together — theory, internals, declaration patterns, common operations, gotchas, real patterns, and many examples you can copy-paste right now.
1. Core Facts About Slices in Go
| Property | Explanation | Consequence / Why it matters |
|---|---|---|
| Type notation | []T (no size inside brackets) | [5]int is array, []int is slice |
| Zero value | nil | Very useful — var s []int is nil |
| Reference type | Slice is a descriptor (pointer + len + cap) — cheap to copy, shares underlying array | Assigning slice = cheap, modifying affects original |
| Dynamic size | Can grow with append, shrink with slicing | Most flexible collection |
| Backed by array | Every slice references some portion of an underlying array | Understanding this prevents many bugs |
| len & cap | len(s) = visible elements, cap(s) = underlying capacity until reallocation | Key to performance |
| Most used collection | Used for lists, queues, stacks, buffers, JSON arrays, HTTP params, database rows, etc. | You will write []string, []int, []byte every day |
Golden sentence to remember forever:
A slice is a window (view) into an underlying array. It has three fields internally:
- pointer to the start of the window
- length (how many elements you can see)
- capacity (how far the window can grow without reallocation)
2. How to Create Slices (5 Most Common Ways)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
package main import "fmt" func main() { // 1. Literal (most readable & common) fruits := []string{"apple", "banana", "cherry"} // creates array + slice on top // 2. make() — pre-allocate length & capacity scores := make([]int, 5) // len=5, cap=5, all zeros buffer := make([]byte, 0, 1024) // len=0, cap=1024 — very common pattern // 3. From array (slice header on existing array) daysArray := [7]string{"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"} daysSlice := daysArray[:] // full slice of array // 4. Zero value / nil slice var empty []int // nil slice — len=0, cap=0 // 5. Slicing an existing slice (very powerful) numbers := []int{10, 20, 30, 40, 50, 60} middle := numbers[2:5] // [30, 40, 50] len=3, cap=4 fmt.Printf("fruits: %v (len=%d, cap=%d)\n", fruits, len(fruits), cap(fruits)) fmt.Printf("scores: %v (len=%d, cap=%d)\n", scores, len(scores), cap(scores)) fmt.Printf("buffer: len=%d, cap=%d\n", len(buffer), cap(buffer)) fmt.Printf("middle: %v (len=%d, cap=%d)\n", middle, len(middle), cap(middle)) fmt.Printf("nil slice: %v (len=%d, cap=%d) is nil? %t\n", empty, len(empty), cap(empty), empty == nil) } |
Typical output:
|
0 1 2 3 4 5 6 7 8 9 10 |
fruits: [apple banana cherry] (len=3, cap=3) scores: [0 0 0 0 0] (len=5, cap=5) buffer: len=0, cap=1024 middle: [30 40 50] (len=3, cap=4) nil slice: [] (len=0, cap=0) is nil? true |
3. The Three Most Important Operations
a) append() — grows the slice (most used function)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 |
s := []int{1, 2, 3} s = append(s, 4, 5) // [1 2 3 4 5] s = append(s, 6) // may cause reallocation if cap full // Common pattern: append to nil slice var logs []string logs = append(logs, "User logged in", "Page viewed") |
Important rules:
- append may return a new slice header → always do s = append(s, …)
- If cap is enough → no reallocation (fast)
- If cap is full → new larger underlying array (usually ~double size)
b) Slicing — create new views (very cheap)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 |
data := []int{10, 20, 30, 40, 50, 60, 70} firstThree := data[:3] // [10 20 30] cap still 7 lastTwo := data[5:] // [60 70] cap=2 middle := data[2:5] // [30 40 50] cap=5 copyOnly := data[2:5:5] // [30 40 50] cap=3 (full slice expression) fmt.Printf("middle: %v (cap=%d)\n", middle, cap(middle)) |
Full slice expression [low:high:max] → controls new capacity (very useful to prevent accidental modification of tail).
c) copy() — copy elements between slices
|
0 1 2 3 4 5 6 7 8 9 10 11 |
src := []int{1, 2, 3, 4, 5} dst := make([]int, 3) // len=3 n := copy(dst, src) // copies min(len(dst), len(src)) elements fmt.Println(dst) // [1 2 3] fmt.Println("Copied:", n) // 3 |
4. Classic Gotchas & Safety Tips
- Appending to slice from slice can share memory
|
0 1 2 3 4 5 6 7 8 9 |
a := []int{1, 2, 3, 4, 5} b := a[0:3] // [1,2,3] cap=5 b = append(b, 99) // may overwrite a[3] if no reallocation! fmt.Println(a) // could become [1 2 3 99 5] !!! |
Fix: use full slice expression or copy
|
0 1 2 3 4 5 6 7 8 9 |
b := append([]int{}, a[0:3]...) // safe new copy // or b := make([]int, 3) copy(b, a) |
- nil vs empty slice
|
0 1 2 3 4 5 6 7 8 9 10 |
var nilSlice []int // nil emptySlice := []int{} // non-nil, len=0, cap=0 fmt.Println(nilSlice == nil) // true fmt.Println(emptySlice == nil) // false |
Both behave same in most operations (append, len, range), but nil is preferred when you mean “no value”.
5. Your Quick Practice Exercise (Try Now)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
package main import "fmt" func main() { // 1. Build a dynamic list var tasks []string tasks = append(tasks, "Learn slices", "Practice append", "Understand cap") fmt.Printf("Tasks: %q (len=%d, cap=%d)\n", tasks, len(tasks), cap(tasks)) // 2. Slice window numbers := []int{10, 20, 30, 40, 50, 60, 70, 80} window := numbers[3:7] // 40 50 60 70 fmt.Printf("Window: %v (cap=%d)\n", window, cap(window)) // 3. Safe copy backup := make([]int, len(numbers)) copy(backup, numbers) backup[0] = 999 fmt.Println("Original after backup change:", numbers) // unchanged } |
Play with it:
- Append more elements — watch cap grow
- Slice with [low:high:high] syntax
- Try append on sub-slice → see shared memory bug
Questions now?
- Deep dive on slice internals (3-word structure)?
- append growth strategy / amortized complexity?
- 2D slices ([][]int)?
- strings.Split, bytes.Split, JSON arrays?
- Or next: structs or maps?
Slices are the most important composite type in everyday Go — once you feel comfortable with append, slicing, make, copy, and nil vs empty, you’ll write much safer & idiomatic code.
Keep running examples — you’re doing fantastic! 💪🇮🇳 Let’s keep going! 🚀
