Slice #

Slices are the data type you’ll use most often in Go — more than arrays, more than maps. Almost every data collection in Go is expressed as a slice. But a slice isn’t just a “resizable array.” Behind it is a three-component mechanism you need to understand to avoid the subtlest and most common bug in Go: the shared backing array. This article covers slices from how they work in memory to all the idiomatic operations used in production code.

Slice Anatomy — Three Internal Components #

Every slice variable in Go stores three fields in a small struct called the slice header. This internal data structure and its relationship with the backing array in memory can be visualized as follows:

flowchart TD
    subgraph SliceHeader["Slice Header (Internal Structure)"]
        direction LR
        Ptr["Pointer (Memory Start Address)"]
        Len["Len (Slice Length)"]
        Cap["Cap (Slice Capacity)"]
    end

    subgraph BackingArray["Backing Array in Memory"]
        E0["e0"]
        E1["e1"]
        E2["e2"]
        E3["e3"]
        E4["e4"]
        E5["e5"]
        E6["e6"]
        E7["e7"]
    end

    Ptr --> E0
    
    E0 -.->|"Length (Len)"| E2
    E0 -.->|"Capacity (Cap)"| E7
  • Pointer — points to the first element “visible” to this slice inside the backing array
  • Len (length) — the number of elements currently in the slice; the ones you can access by index
  • Cap (capacity) — the number of elements available from the pointer position to the end of the backing array
s := []int{10, 20, 30, 40, 50}
fmt.Println(len(s))  // 5 — length
fmt.Println(cap(s))  // 5 — capacity = backing array length from the pointer

// Sub-slicing moves the pointer; len changes, cap shrinks
sub := s[1:3]        // {20, 30}
fmt.Println(len(sub))  // 2
fmt.Println(cap(sub))  // 4 — from index 1 to the end of the backing array (5-1=4)

Ways to Create a Slice #

Literals #

// Most common for already-known data
numbers := []int{1, 2, 3, 4, 5}
names := []string{"Budi", "Sari", "Ahmad"}
empty := []int{}  // empty slice — not nil

make([]T, len, cap) — Pre-allocation #

Use make when the final length or capacity can be estimated:

// A slice with len=5, all elements zero values
s1 := make([]int, 5)         // len=5, cap=5
fmt.Println(s1)               // [0 0 0 0 0]

// A slice with len=0 but cap=100 — ready to hold up to 100 elements without re-allocation
s2 := make([]int, 0, 100)    // len=0, cap=100
fmt.Println(len(s2), cap(s2)) // 0 100

// Useful when you know how many elements will be added
result := make([]int, 0, len(input))
for _, v := range input {
    if v > 0 {
        result = append(result, v)
    }
}

From an Array #

arr := [5]int{10, 20, 30, 40, 50}
s := arr[1:4]    // {20, 30, 40} — shares the backing array with arr

Nil Slice vs Empty Slice #

var nilSlice []int    // nil slice — pointer=nil, len=0, cap=0
emptySlice := []int{} // empty slice — non-nil pointer, len=0, cap=0

fmt.Println(nilSlice == nil)    // true
fmt.Println(emptySlice == nil)  // false

// Both have len=0 and can be appended to
fmt.Println(len(nilSlice))   // 0
fmt.Println(len(emptySlice)) // 0

// CRITICAL DIFFERENCE: JSON serialization
import "encoding/json"

data1, _ := json.Marshal(nilSlice)    // null
data2, _ := json.Marshal(emptySlice)  // []

fmt.Println(string(data1))  // null
fmt.Println(string(data2))  // []
Nil slices and empty slices produce different JSON. If your API needs to return an empty array (not null), make sure to use []T{} or make([]T, 0), not var s []T. JavaScript clients receiving null instead of [] often error because null.map() is invalid.

Slicing Expressions #

You can take a portion of a slice or array using the s[low:high] syntax:

s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}

s1 := s[2:5]    // {2, 3, 4}       — from index 2 to 4 (not including 5)
s2 := s[:3]     // {0, 1, 2}       — from the start to index 2
s3 := s[7:]     // {7, 8, 9}       — from index 7 to the end
s4 := s[:]      // {0,...,9}        — the whole slice (new header, same backing array)

// Rule: 0 <= low <= high <= cap(s)

Three-Index Slicing — Limiting Capacity #

Three-index slicing s[low:high:max] lets you control the resulting slice’s capacity:

s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}

// Two-index: cap follows the rest of the backing array
s1 := s[2:5]       // len=3, cap=8  (from index 2 to the end)

// Three-index: cap is limited
s2 := s[2:5:6]     // len=3, cap=4  (from index 2, max index 6)

fmt.Println(len(s1), cap(s1))  // 3 8
fmt.Println(len(s2), cap(s2))  // 3 4

// Why is this useful? Appending to s2 won't "corrupt" elements of s beyond index 6

Shared Backing Arrays — The Most Important Gotcha #

This is the most common source of bugs with slices. When you slice, the new slice shares the same backing array as the original:

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

// Modifying sub CHANGES original!
sub[0] = 999
fmt.Println(original)  // [1 999 3 4 5] — changed!
fmt.Println(sub)       // [999 3]

// And vice versa
original[2] = 777
fmt.Println(sub)       // [999 777] — sub changed too!

When This Becomes a Bug #

// ANTI-PATTERN: a function modifying an external slice without realizing it
func processFirst3(data []int) []int {
    result := data[:3]
    result[0] = 0    // MODIFIES the ORIGINAL data! The caller doesn't expect this
    return result
}

// CORRECT: create an independent copy
func processFirst3Safe(data []int) []int {
    if len(data) < 3 {
        return nil
    }
    result := make([]int, 3)
    copy(result, data[:3])   // copy creates a new backing array
    result[0] = 0            // only modifies result, not the original data
    return result
}

append — How It Works and Gotchas #

append adds elements to a slice and returns the new slice:

s := []int{1, 2, 3}
s = append(s, 4)        // add one element
s = append(s, 5, 6, 7)  // add several elements at once

// Spread operator — combine two slices
a := []int{1, 2, 3}
b := []int{4, 5, 6}
c := append(a, b...)
fmt.Println(c)  // [1 2 3 4 5 6]

When append Allocates a New Backing Array #

This is important to understand:

s := make([]int, 3, 5)  // len=3, cap=5
fmt.Printf("ptr=%p, len=%d, cap=%d\n", &s[0], len(s), cap(s))

s = append(s, 4)  // still has room (cap=5, len is now 4)
fmt.Printf("ptr=%p, len=%d, cap=%d\n", &s[0], len(s), cap(s))
// SAME ptr — the backing array didn't change!

s = append(s, 5)  // full! (cap=5, len is now 5)
s = append(s, 6)  // exceeds cap → allocates a NEW backing array (cap ≈ 2x)
fmt.Printf("ptr=%p, len=%d, cap=%d\n", &s[0], len(s), cap(s))
// DIFFERENT ptr — new backing array with cap ≈ 10

Gotcha: append Doesn’t Always Preserve Sharing #

a := []int{1, 2, 3, 4, 5}
b := a[:3]  // b shares the backing array with a
c := a[:3]  // c also shares the same backing array

// Append to b while there's still capacity
b = append(b, 99)   // capacity is enough → modifies a's backing array!
fmt.Println(a)      // [1 2 3 99 5] — a changed!
fmt.Println(b)      // [1 2 3 99]
fmt.Println(c)      // [1 2 3] — c is still [1 2 3] (len=3, doesn't "see" the 4th element)

// Append to b again after capacity is full
b = append(b, 88, 77, 66)  // exceeds capacity → NEW backing array
b[0] = 0            // now doesn't affect a
fmt.Println(a)      // [1 2 3 99 5] — a unchanged

Always store the result of append back to a variable. append may return a slice with a different backing array than the input. If you don’t store the result, all additions are lost.

// ANTI-PATTERN: append result ignored
func addItem(s []int, item int) {
    append(s, item)  // ✗ result discarded! no effect
}

// CORRECT: return the new slice
func addItem(s []int, item int) []int {
    return append(s, item)  // ✓
}

copy — Creating Independent Slices #

copy(dst, src) copies elements from src to dst and returns the number of elements copied (the minimum of len(dst) and len(src)):

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

// Full copy
dst := make([]int, len(src))
n := copy(dst, src)
fmt.Println(dst, n)  // [1 2 3 4 5] 5

// Modifying dst doesn't affect src
dst[0] = 999
fmt.Println(src)  // [1 2 3 4 5] — unchanged!

// Partial copy — copy takes the minimum of len(dst) and len(src)
partial := make([]int, 3)
copy(partial, src)       // only 3 elements are copied
fmt.Println(partial)     // [1 2 3]

// Copy between positions in the same slice (overlap is safe)
s := []int{1, 2, 3, 4, 5}
copy(s[1:], s[0:])       // shift all elements right by one position
fmt.Println(s)           // [1 1 2 3 4]

Idiomatic Operations #

Deleting Elements #

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

// Delete the element at index i (order not preserved — faster)
func deleteUnordered(s []int, i int) []int {
    s[i] = s[len(s)-1]  // move the last element to position i
    return s[:len(s)-1]  // reduce the length
}

// Delete the element at index i (preserving order)
func deleteOrdered(s []int, i int) []int {
    return append(s[:i], s[i+1:]...)
}

// Example
s = deleteOrdered(s, 2)
fmt.Println(s)  // [1 2 4 5]

Inserting Elements #

// Insert value v at index i
func insert(s []int, i int, v int) []int {
    s = append(s, 0)           // make room at the end
    copy(s[i+1:], s[i:])       // shift elements right
    s[i] = v                   // fill position i
    return s
}

s := []int{1, 2, 4, 5}
s = insert(s, 2, 3)  // insert 3 at index 2
fmt.Println(s)        // [1 2 3 4 5]

Filtering — Keep Elements Matching a Condition #

// In-place filter — reuse the backing array, more memory-efficient
func filter(s []int, keep func(int) bool) []int {
    result := s[:0]  // a slice with len=0, cap=cap(s), same backing array
    for _, v := range s {
        if keep(v) {
            result = append(result, v)
        }
    }
    return result
}

numbers := []int{1, -2, 3, -4, 5, -6}
positive := filter(numbers, func(n int) bool { return n > 0 })
fmt.Println(positive)  // [1 3 5]

Deduplicating — Removing Duplicates #

func deduplicate(s []int) []int {
    if len(s) == 0 {
        return s
    }
    seen := make(map[int]bool)
    result := s[:0]
    for _, v := range s {
        if !seen[v] {
            seen[v] = true
            result = append(result, v)
        }
    }
    return result
}

data := []int{3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5}
fmt.Println(deduplicate(data))  // [3 1 4 5 9 2 6]

Reversing #

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]
    }
}

s := []int{1, 2, 3, 4, 5}
reverse(s)
fmt.Println(s)  // [5 4 3 2 1]

Sorting Slices #

The sort package provides functions for sorting slices:

import "sort"

// Integers
numbers := []int{3, 1, 4, 1, 5, 9, 2, 6}
sort.Ints(numbers)
fmt.Println(numbers)  // [1 1 2 3 4 5 6 9]

// Strings
words := []string{"banana", "apple", "cherry", "date"}
sort.Strings(words)
fmt.Println(words)  // [apple banana cherry date]

// Floats
values := []float64{3.14, 1.41, 2.71, 1.73}
sort.Float64s(values)
fmt.Println(values)  // [1.41 1.73 2.71 3.14]

// Custom — sort.Slice with a less function
type Person struct {
    Name string
    Age  int
}

people := []Person{
    {"Charlie", 30},
    {"Alice", 25},
    {"Bob", 35},
}

// Sort by name
sort.Slice(people, func(i, j int) bool {
    return people[i].Name < people[j].Name
})
fmt.Println(people)  // [{Alice 25} {Bob 35} {Charlie 30}]

// Sort by age (descending)
sort.Slice(people, func(i, j int) bool {
    return people[i].Age > people[j].Age  // > for descending
})
fmt.Println(people)  // [{Bob 35} {Charlie 30} {Alice 25}]

// Check whether already sorted
fmt.Println(sort.IntsAreSorted([]int{1, 2, 3, 4}))  // true
fmt.Println(sort.IntsAreSorted([]int{1, 3, 2, 4}))  // false

// Binary search on a sorted slice
sort.Ints(numbers)
i, found := sort.Find(len(numbers), func(i int) int {
    return numbers[i] - 5  // search for 5
})
fmt.Println(i, found)  // the index and whether it was found

Pre-allocation for Performance #

Re-allocating a backing array when append exceeds capacity is an expensive operation (new memory allocation + copying all elements). If you know how many elements there will be, pre-allocating with make avoids repeated re-allocation:

// ANTI-PATTERN: repeated re-allocation
func buildSliceSlow(n int) []int {
    var result []int              // cap=0
    for i := 0; i < n; i++ {
        result = append(result, i)  // re-allocates ~log(n) times!
    }
    return result
}

// CORRECT: pre-allocate once
func buildSliceFast(n int) []int {
    result := make([]int, 0, n)   // cap=n from the start
    for i := 0; i < n; i++ {
        result = append(result, i)  // never re-allocates
    }
    return result
}

// Or if the final length equals the capacity:
func buildSliceDirect(n int) []int {
    result := make([]int, n)    // len=n, all zeros
    for i := range result {
        result[i] = i           // assign directly, no append needed
    }
    return result
}

Complete Example Program #

The following program builds a simple inventory system using various slice operations:

package main

import (
    "fmt"
    "sort"
    "strings"
)

type Product struct {
    ID       int
    Name     string
    Category string
    Price    float64
    Stock    int
}

type Inventory struct {
    products []Product
    nextID   int
}

func NewInventory() *Inventory {
    return &Inventory{
        products: make([]Product, 0, 16), // pre-allocation
    }
}

func (inv *Inventory) Add(name, category string, price float64, stock int) {
    inv.nextID++
    inv.products = append(inv.products, Product{
        ID:       inv.nextID,
        Name:     name,
        Category: category,
        Price:    price,
        Stock:    stock,
    })
}

// Filter products by criteria
func (inv *Inventory) Filter(keep func(Product) bool) []Product {
    result := make([]Product, 0)
    for _, p := range inv.products {
        if keep(p) {
            result = append(result, p)
        }
    }
    return result
}

// Remove a product by ID
func (inv *Inventory) Remove(id int) bool {
    for i, p := range inv.products {
        if p.ID == id {
            // Remove while preserving order
            inv.products = append(inv.products[:i], inv.products[i+1:]...)
            return true
        }
    }
    return false
}

// Update stock
func (inv *Inventory) UpdateStock(id, delta int) error {
    for i := range inv.products {
        if inv.products[i].ID == id {
            newStock := inv.products[i].Stock + delta
            if newStock < 0 {
                return fmt.Errorf("insufficient stock: %d available, %d being removed",
                    inv.products[i].Stock, -delta)
            }
            inv.products[i].Stock = newStock
            return nil
        }
    }
    return fmt.Errorf("product ID %d not found", id)
}

// Get all unique categories
func (inv *Inventory) Categories() []string {
    seen := make(map[string]bool)
    var cats []string
    for _, p := range inv.products {
        if !seen[p.Category] {
            seen[p.Category] = true
            cats = append(cats, p.Category)
        }
    }
    sort.Strings(cats)
    return cats
}

// Sort products by a specific field
func (inv *Inventory) SortBy(field string, ascending bool) {
    sort.Slice(inv.products, func(i, j int) bool {
        a, b := inv.products[i], inv.products[j]
        var less bool
        switch field {
        case "name":
            less = a.Name < b.Name
        case "price":
            less = a.Price < b.Price
        case "stock":
            less = a.Stock < b.Stock
        default:
            less = a.ID < b.ID
        }
        if ascending {
            return less
        }
        return !less
    })
}

// Summary report
func (inv *Inventory) Summary() {
    if len(inv.products) == 0 {
        fmt.Println("Inventory is empty")
        return
    }

    // Calculate statistics using slice operations
    totalValue := 0.0
    lowStock := inv.Filter(func(p Product) bool { return p.Stock < 5 })
    outOfStock := inv.Filter(func(p Product) bool { return p.Stock == 0 })

    for _, p := range inv.products {
        totalValue += p.Price * float64(p.Stock)
    }

    fmt.Printf("Total products      : %d\n", len(inv.products))
    fmt.Printf("Total stock value   : Rp%.0f\n", totalValue)
    fmt.Printf("Low stock (<5)      : %d products\n", len(lowStock))
    fmt.Printf("Out of stock        : %d products\n", len(outOfStock))
    fmt.Printf("Categories          : %s\n", strings.Join(inv.Categories(), ", "))
}

// Print a product table
func printProducts(products []Product, title string) {
    if len(products) == 0 {
        fmt.Printf("\n%s: (empty)\n", title)
        return
    }
    fmt.Printf("\n%s:\n", title)
    fmt.Printf("  %-4s %-20s %-12s %10s %6s\n",
        "ID", "Name", "Category", "Price", "Stock")
    fmt.Println("  " + strings.Repeat("-", 58))
    for _, p := range products {
        fmt.Printf("  %-4d %-20s %-12s %10.0f %6d\n",
            p.ID, p.Name, p.Category, p.Price, p.Stock)
    }
}

func main() {
    inv := NewInventory()

    // Add products
    inv.Add("Pro Laptop 14",    "Electronics", 15_000_000, 10)
    inv.Add("Wireless Mouse",   "Electronics",    350_000,  3)
    inv.Add("Mech Keyboard",    "Electronics",  1_500_000,  7)
    inv.Add("27\" Monitor",     "Electronics",  5_000_000,  2)
    inv.Add("Plain T-Shirt",    "Fashion",        85_000, 50)
    inv.Add("Chino Pants",      "Fashion",       250_000, 30)
    inv.Add("Bomber Jacket",    "Fashion",       450_000,  4)
    inv.Add("Go Language Book", "Books",         180_000, 15)
    inv.Add("Clean Code Book",  "Books",         220_000,  0)

    // Show all products
    printProducts(inv.products, "All Products (insertion order)")

    // Sort by price — ascending
    inv.SortBy("price", true)
    printProducts(inv.products, "Sorted by Price (cheap to expensive)")

    // Filter — electronics only
    electronics := inv.Filter(func(p Product) bool {
        return p.Category == "Electronics"
    })
    printProducts(electronics, "Electronics Products")

    // Filter — low stock
    low := inv.Filter(func(p Product) bool {
        return p.Stock > 0 && p.Stock < 5
    })
    printProducts(low, "Low Stock (1-4 units)")

    // Slice operations
    fmt.Println("\n=== Slice Operations ===")

    // Take the 3 most expensive products — sort descending first
    inv.SortBy("price", false)
    top3 := inv.products[:3]  // slicing — shares the backing array!
    fmt.Println("3 Most Expensive Products:")
    for i, p := range top3 {
        fmt.Printf("  %d. %s — Rp%.0f\n", i+1, p.Name, p.Price)
    }

    // Create an independent copy for safe modification
    top3Copy := make([]Product, len(top3))
    copy(top3Copy, top3)
    top3Copy[0].Price = 0  // only changes the copy, not inv.products!
    fmt.Printf("Original price after modifying the copy: Rp%.0f\n",
        inv.products[0].Price)  // unchanged

    // Update stock
    fmt.Println("\n=== Stock Updates ===")
    if err := inv.UpdateStock(1, -3); err != nil {
        fmt.Println("Error:", err)
    } else {
        fmt.Println("Successfully reduced Pro Laptop 14 stock")
    }

    // Try reducing stock by more than available
    if err := inv.UpdateStock(2, -10); err != nil {
        fmt.Println("Error:", err)
    }

    // Remove a product
    removed := inv.Remove(9)  // remove "Clean Code Book" which has 0 stock
    fmt.Printf("\nRemoved product ID 9: %v\n", removed)

    // Final summary
    fmt.Println("\n=== Inventory Summary ===")
    inv.Summary()
}

Summary #

  • Three-component slice header: pointer, len, cap — understanding this is the key to understanding all slice behavior.
  • Nil slices vs empty slices: var s []T (nil, JSON null) vs s := []T{} (empty, JSON []) — different for serialization.
  • Slicing shares the backing array — modifying a sub-slice affects the original slice; use copy for independent slices.
  • Always reassign append: s = append(s, v) — don’t append(s, v) without storing the result.
  • append can allocate a new backing array — when capacity is exceeded; after that, old sub-slices no longer share memory.
  • copy(dst, src) copies min(len(dst), len(src)) elements — always create dst with a sufficiently large make.
  • Pre-allocate with make([]T, 0, n) when the element count can be estimated — avoids repeated re-allocation.
  • sort.Slice with a custom less function sorts structs by any field.
  • In-place filtering with result := s[:0] — reuses the backing array without new allocations.
  • Three-index slicing s[low:high:max] controls the resulting slice’s capacity and prevents accidental appends from modifying elements outside the range.

← Previous: Array   Next: Map →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact