GO

Go Fundamentals

18 lessons

Progress0%
1. Introduction to Go
1What is Go?
2. Variables and Data Types
1Data Types in Go
3. Control Flow
If, For, and SwitchDefer, Panic, Recover
4. Functions
Function BasicsError Handling
5. Structs and Methods
StructsMethods and Interfaces
6. Concurrency
Goroutines and ChannelsSelect and Sync
7. Maps & Slices Advanced
Slices Deep DiveMaps Operations & Patterns
8. Interfaces Deep Dive
Interface Composition & anyCommon Interfaces & Patterns
9. Packages & Modules
Package SystemGo Modules & Workspace
10. Testing & Standard Library
Testing in GoStandard Library Essentials
All Tutorials
GoMaps & Slices Advanced
Lesson 11 of 18 min
Chapter 7 · Lesson 1

Slices Deep Dive

Slices Deep Dive

A slice in Go is a lightweight data structure that wraps an underlying array. Understanding its internals helps you write efficient, bug-free code.

Slice Internals Every slice is a three-field descriptor:

  • Pointer: points to the first element of the underlying array
  • Length (len): the number of elements the slice currently holds
  • Capacity (cap): the number of elements in the underlying array starting from the slice's pointer

When you take a sub-slice s[1:3], the result shares the same underlying array. Mutating the sub-slice mutates the original — a frequent source of bugs.

make([]T, len, cap) Use make when you know the size in advance. Providing a capacity avoids repeated allocations during append:

go
s := make([]int, 0, 100) // len=0, cap=100

append and Growth append returns a new slice header. If the backing array has room, no allocation occurs. When capacity is exhausted, Go allocates a new array (typically doubling capacity for small slices) and copies existing elements. Always capture the return value:

go
s = append(s, newElement)

copy(dst, src) copy copies min(len(dst), len(src)) elements and returns the number copied. It never grows slices automatically, making it predictable for buffer management.

Slice Tricks Several common patterns arise repeatedly:

Delete element at index i (order preserved):

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

Delete element at index i (order not preserved, faster):

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

Insert element at index i:

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

Reverse a slice in place:

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

2D Slices A slice of slices models a matrix. Each inner slice can have a different length (a jagged array):

go
matrix := make([][]int, rows)
for i := range matrix {
    matrix[i] = make([]int, cols)
}

range Over Slices range returns both the index and a copy of the value. Modify the original through indexing, not through the range variable:

go
for i, v := range s {
    fmt.Println(i, v)
}

nil vs Empty Slice

  • var s []int — nil slice; s == nil is true; len and cap are 0
  • s := []int{} or make([]int, 0) — non-nil empty slice; s == nil is false

Both are safe to append to and to range over. Prefer nil slices as zero values; prefer non-nil empty slices when you must distinguish "no data yet" from "explicitly empty" (e.g., JSON serialisation: nil encodes to null, empty encodes to []).

Code Examples

make, append and capacity growthgo
package main

import "fmt"

func main() {
    // Literal vs make
    literal := []int{1, 2, 3}
    made := make([]int, 3, 6) // len=3, cap=6
    fmt.Printf("literal: len=%d cap=%d\n", len(literal), cap(literal))
    fmt.Printf("made:    len=%d cap=%d\n", len(made), cap(made))

    // Append and growth — watch capacity jumps
    var s []int
    for i := 0; i < 9; i++ {
        s = append(s, i)
        fmt.Printf("len=%d cap=%d\n", len(s), cap(s))
    }

    // Sub-slice shares backing array
    a := []int{10, 20, 30, 40, 50}
    b := a[1:3] // shares array with a
    b[0] = 99
    fmt.Println("a after mutating b:", a) // [10 99 30 40 50]
}

Capacity doubles when exhausted, amortising allocation cost. Sub-slices share memory, so mutation is visible through the original.

copy and common slice tricksgo
package main

import "fmt"

func deleteAt(s []int, i int) []int {
    return append(s[:i], s[i+1:]...)
}

func insertAt(s []int, i, v int) []int {
    s = append(s, 0)
    copy(s[i+1:], s[i:])
    s[i] = v
    return s
}

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

func main() {
    s := []int{1, 2, 3, 4, 5}

    // copy into a brand-new backing array
    clone := make([]int, len(s))
    copy(clone, s)
    clone[0] = 99
    fmt.Println("original:", s)    // unaffected
    fmt.Println("clone:   ", clone)

    // delete index 2
    s = deleteAt(s, 2)
    fmt.Println("after delete:", s) // [1 2 4 5]

    // insert 99 at index 1
    s = insertAt(s, 1, 99)
    fmt.Println("after insert:", s) // [1 99 2 4 5]

    // reverse
    reverse(s)
    fmt.Println("reversed:", s) // [5 4 2 99 1]
}

copy creates an independent slice. The delete trick re-uses the backing array in place. insertAt shifts elements right with copy before writing the new value.

2D slices — matrix examplego
package main

import "fmt"

func newMatrix(rows, cols int) [][]int {
    m := make([][]int, rows)
    for i := range m {
        m[i] = make([]int, cols)
    }
    return m
}

func printMatrix(m [][]int) {
    for _, row := range m {
        fmt.Println(row)
    }
}

func main() {
    m := newMatrix(3, 4)

    // Fill with row*10 + col
    for r := range m {
        for c := range m[r] {
            m[r][c] = r*10 + c
        }
    }
    printMatrix(m)

    // Jagged slice — each row has a different length
    triangle := make([][]int, 4)
    for i := range triangle {
        triangle[i] = make([]int, i+1)
        for j := range triangle[i] {
            triangle[i][j] = j + 1
        }
    }
    fmt.Println("--- triangle ---")
    printMatrix(triangle)
}

A 2D slice is a slice of slices. Each inner slice is allocated independently, so rows can have different lengths — a jagged array.

Quick Quiz

1. What three fields make up a slice descriptor in Go?

2. Which statement correctly explains when append allocates a new backing array?

3. What is the key difference between a nil slice and an empty slice in Go?

4. Why must you always capture the return value of append?

Was this lesson helpful?

PreviousNext