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 (notnull), make sure to use[]T{}ormake([]T, 0), notvar s []T. JavaScript clients receivingnullinstead of[]often error becausenull.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
appendback to a variable.appendmay 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, JSONnull) vss := []T{}(empty, JSON[]) — different for serialization.- Slicing shares the backing array — modifying a sub-slice affects the original slice; use
copyfor independent slices.- Always reassign
append:s = append(s, v)— don’tappend(s, v)without storing the result.appendcan allocate a new backing array — when capacity is exceeded; after that, old sub-slices no longer share memory.copy(dst, src)copiesmin(len(dst), len(src))elements — always create dst with a sufficiently largemake.- Pre-allocate with
make([]T, 0, n)when the element count can be estimated — avoids repeated re-allocation.sort.Slicewith 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.