Skip to content

Go Slices: Dynamic Arrays and Memory Management

Overview

Master Go's most versatile data structure - slices. This comprehensive guide covers dynamic arrays, memory management, slice operations, and advanced techniques, building upon our understanding of arrays and data types.

Key Points

  • Slices are dynamic, resizable arrays
  • Reference types with underlying array backing
  • Powerful built-in functions: append, copy, make
  • Memory-efficient with capacity management
  • Essential for most Go programming tasks

Understanding Slices in Go 📦

Slices are Go's answer to dynamic arrays, providing flexibility and efficiency that fixed-size arrays cannot offer. They're reference types that point to underlying arrays with automatic memory management.

Slice Architecture

graph TD
    A[Go Slice] --> B[Pointer to Array]
    A --> C[Length]
    A --> D[Capacity]
    B --> E[Underlying Array]
    E --> F[Element 0]
    E --> G[Element 1]
    E --> H[Element 2]
    E --> I[...]
    style A fill:#999,stroke:#333,stroke-width:2px,color:#000
    style E fill:#e1f5fe,stroke:#01579b,stroke-width:2px

Slice Creation Patterns âš™

Go provides multiple ways to create slices, each suited for different scenarios and performance requirements.

Slice Literals and Declarations

Basic Slice Creation

slice_literals.go
package main

import "fmt"

func main() {
    // Create slice with initial values
    fruits := []string{"apple", "banana", "orange", "grape"}
    numbers := []int{1, 2, 3, 4, 5}

    fmt.Printf("Fruits: %v (len=%d, cap=%d)\n", fruits, len(fruits), cap(fruits))
    fmt.Printf("Numbers: %v (len=%d, cap=%d)\n", numbers, len(numbers), cap(numbers))

    // Output:
    // Fruits: [apple banana orange grape] (len=4, cap=4)
    // Numbers: [1 2 3 4 5] (len=5, cap=5)
}
zero_nil_slices.go
package main

import "fmt"

func main() {
    // Nil slice (zero value)
    var nilSlice []int
    fmt.Printf("Nil slice: %v (len=%d, cap=%d, nil=%t)\n",
        nilSlice, len(nilSlice), cap(nilSlice), nilSlice == nil)

    // Empty slice (not nil)
    emptySlice := []int{}
    fmt.Printf("Empty slice: %v (len=%d, cap=%d, nil=%t)\n",
        emptySlice, len(emptySlice), cap(emptySlice), emptySlice == nil)

    // Output:
    // Nil slice: [] (len=0, cap=0, nil=true)
    // Empty slice: [] (len=0, cap=0, nil=false)
}
type_inference.go
package main

import "fmt"

func main() {
    // Type inferred from elements
    mixed := []interface{}{1, "hello", 3.14, true}

    // Slice of slices
    matrix := [][]int{{1, 2}, {3, 4}, {5, 6}}

    fmt.Printf("Mixed: %v\n", mixed)
    fmt.Printf("Matrix: %v\n", matrix)

    // Output:
    // Mixed: [1 hello 3.14 true]
    // Matrix: [[1 2] [3 4] [5 6]]
}

The make Function

The make function provides precise control over slice length and capacity allocation.

Using make for Slice Creation

make_basic.go
package main

import "fmt"

func main() {
    // make([]Type, length, capacity)
    slice1 := make([]int, 5)      // length=5, capacity=5
    slice2 := make([]int, 3, 10)  // length=3, capacity=10

    fmt.Printf("slice1: %v (len=%d, cap=%d)\n", slice1, len(slice1), cap(slice1))
    fmt.Printf("slice2: %v (len=%d, cap=%d)\n", slice2, len(slice2), cap(slice2))

    // Output:
    // slice1: [0 0 0 0 0] (len=5, cap=5)
    // slice2: [0 0 0] (len=3, cap=10)
}
preallocation.go
package main

import "fmt"

func main() {
    // Pre-allocate for known capacity to avoid reallocations
    expectedSize := 1000

    // Efficient: pre-allocate capacity
    efficientSlice := make([]int, 0, expectedSize)

    // Less efficient: will cause multiple reallocations
    inefficientSlice := []int{}

    fmt.Printf("Efficient slice: len=%d, cap=%d\n",
        len(efficientSlice), cap(efficientSlice))
    fmt.Printf("Inefficient slice: len=%d, cap=%d\n",
        len(inefficientSlice), cap(inefficientSlice))
}

Slicing Operations

Create slices from existing arrays or slices using slice expressions.

Slice Expressions

basic_slicing.go
package main

import "fmt"

func main() {
    original := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}

    // slice[start:end] - end is exclusive
    slice1 := original[2:6]    // [2, 3, 4, 5]
    slice2 := original[:4]     // [0, 1, 2, 3]
    slice3 := original[6:]     // [6, 7, 8, 9]
    slice4 := original[:]      // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

    fmt.Printf("slice1: %v\n", slice1)
    fmt.Printf("slice2: %v\n", slice2)
    fmt.Printf("slice3: %v\n", slice3)
    fmt.Printf("slice4: %v\n", slice4)
}
full_slice_expression.go
package main

import "fmt"

func main() {
    original := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}

    // slice[start:end:capacity] - controls capacity
    slice1 := original[2:6:8]  // start=2, end=6, cap=8-2=6
    slice2 := original[1:4:6]  // start=1, end=4, cap=6-1=5

    fmt.Printf("slice1: %v (len=%d, cap=%d)\n", slice1, len(slice1), cap(slice1))
    fmt.Printf("slice2: %v (len=%d, cap=%d)\n", slice2, len(slice2), cap(slice2))

    // Output:
    // slice1: [2 3 4 5] (len=4, cap=6)
    // slice2: [1 2 3] (len=3, cap=5)
}

Slice Operations and Manipulation 🔄

Accessing and Modifying Elements

Element Operations

element_access.go
package main

import "fmt"

func main() {
    colors := []string{"red", "green", "blue", "yellow"}

    // Access elements
    fmt.Printf("First color: %s\n", colors[0])
    fmt.Printf("Last color: %s\n", colors[len(colors)-1])

    // Modify elements
    colors[1] = "purple"
    fmt.Printf("Modified slice: %v\n", colors)

    // Safe access with bounds checking
    if len(colors) > 2 {
        fmt.Printf("Third color: %s\n", colors[2])
    }
}
range_iteration.go
package main

import "fmt"

func main() {
    scores := []int{95, 87, 92, 78, 88}

    // Iterate with index and value
    for i, score := range scores {
        fmt.Printf("Student %d: %d\n", i+1, score)
    }

    // Iterate values only
    total := 0
    for _, score := range scores {
        total += score
    }
    fmt.Printf("Average: %.2f\n", float64(total)/float64(len(scores)))
}

The append Function

The append function is fundamental for growing slices dynamically.

Append Operations

basic_append.go
package main

import "fmt"

func main() {
    var numbers []int

    // Append single elements
    numbers = append(numbers, 1)
    numbers = append(numbers, 2, 3, 4)

    // Append another slice
    moreNumbers := []int{5, 6, 7}
    numbers = append(numbers, moreNumbers...)

    fmt.Printf("Numbers: %v (len=%d, cap=%d)\n",
        numbers, len(numbers), cap(numbers))

    // Output: Numbers: [1 2 3 4 5 6 7] (len=7, cap=8)
}
capacity_growth.go
package main

import "fmt"

func main() {
    var slice []int

    // Observe capacity growth
    for i := 0; i < 10; i++ {
        slice = append(slice, i)
        fmt.Printf("len=%d, cap=%d, slice=%v\n",
            len(slice), cap(slice), slice)
    }

    // Capacity typically doubles when exceeded
    // Output shows: cap=1, 2, 4, 8, 16...
}
efficient_append.go
package main

import "fmt"

func main() {
    // Pre-allocate when size is known
    expectedSize := 1000
    slice := make([]int, 0, expectedSize)

    // Efficient append within capacity
    for i := 0; i < expectedSize; i++ {
        slice = append(slice, i)
    }

    fmt.Printf("Final: len=%d, cap=%d\n", len(slice), cap(slice))

    // Append beyond capacity triggers reallocation
    slice = append(slice, 1000)
    fmt.Printf("After overflow: len=%d, cap=%d\n", len(slice), cap(slice))
}

The copy Function

The copy function provides safe copying between slices.

Copy Operations

basic_copy.go
package main

import "fmt"

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

    // Create destination slice
    dest := make([]int, len(source))

    // Copy elements
    copied := copy(dest, source)

    fmt.Printf("Source: %v\n", source)
    fmt.Printf("Destination: %v\n", dest)
    fmt.Printf("Elements copied: %d\n", copied)

    // Modify destination to show independence
    dest[0] = 100
    fmt.Printf("After modification - Source: %v, Dest: %v\n", source, dest)
}
partial_copy.go
package main

import "fmt"

func main() {
    source := []int{1, 2, 3, 4, 5, 6, 7, 8}

    // Copy to smaller destination
    smallDest := make([]int, 3)
    copied1 := copy(smallDest, source)

    // Copy from larger source to larger destination
    largeDest := make([]int, 10)
    copied2 := copy(largeDest, source)

    fmt.Printf("Small dest: %v (copied %d)\n", smallDest, copied1)
    fmt.Printf("Large dest: %v (copied %d)\n", largeDest, copied2)
}
overlapping_copy.go
package main

import "fmt"

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

    // Copy within same slice (shift elements)
    copy(slice[2:], slice[0:4])  // Copy [1,2,3,4] to positions 2-5

    fmt.Printf("After overlapping copy: %v\n", slice)
    // Output: After overlapping copy: [1 2 1 2 3 4]
}

Advanced Slice Techniques âš™

Memory Management and Performance

Memory Optimization

memory_sharing.go
package main

import "fmt"

func main() {
    original := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

    // Slices share underlying array
    slice1 := original[2:5]  // [3, 4, 5]
    slice2 := original[4:7]  // [5, 6, 7]

    fmt.Printf("Original: %v\n", original)
    fmt.Printf("Slice1: %v\n", slice1)
    fmt.Printf("Slice2: %v\n", slice2)

    // Modifying slice1 affects original and potentially slice2
    slice1[2] = 99  // Changes original[4] and slice2[0]

    fmt.Printf("After modification:\n")
    fmt.Printf("Original: %v\n", original)
    fmt.Printf("Slice1: %v\n", slice1)
    fmt.Printf("Slice2: %v\n", slice2)
}
memory_leaks.go
package main

import "fmt"

func processLargeSlice() []int {
    largeSlice := make([]int, 1000000)
    // ... populate largeSlice ...

    // BAD: Returns slice that holds reference to large array
    // return largeSlice[0:10]

    // GOOD: Copy to new slice to release large array
    result := make([]int, 10)
    copy(result, largeSlice[0:10])
    return result
}

func main() {
    small := processLargeSlice()
    fmt.Printf("Small slice: %v (len=%d, cap=%d)\n",
        small, len(small), cap(small))
}

Slice Manipulation Patterns

Common Patterns

insert_element.go
package main

import "fmt"

func insertAt(slice []int, index, value int) []int {
    // Grow slice by one element
    slice = append(slice, 0)

    // Shift elements to the right
    copy(slice[index+1:], slice[index:])

    // Insert new value
    slice[index] = value
    return slice
}

func main() {
    numbers := []int{1, 2, 4, 5}
    numbers = insertAt(numbers, 2, 3)
    fmt.Printf("After insert: %v\n", numbers)  // [1 2 3 4 5]
}
remove_element.go
package main

import "fmt"

func removeAt(slice []int, index int) []int {
    // Shift elements to the left
    copy(slice[index:], slice[index+1:])

    // Shrink slice
    return slice[:len(slice)-1]
}

func main() {
    numbers := []int{1, 2, 3, 4, 5}
    numbers = removeAt(numbers, 2)
    fmt.Printf("After remove: %v\n", numbers)  // [1 2 4 5]
}
filter_slice.go
package main

import "fmt"

func filter(slice []int, predicate func(int) bool) []int {
    result := make([]int, 0, len(slice))

    for _, value := range slice {
        if predicate(value) {
            result = append(result, value)
        }
    }

    return result
}

func main() {
    numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

    // Filter even numbers
    evens := filter(numbers, func(n int) bool { return n%2 == 0 })
    fmt.Printf("Even numbers: %v\n", evens)  // [2 4 6 8 10]
}

Best Practices and Performance 📓

Slice Best Practices

  1. Pre-allocate When Size is Known

    1
    2
    3
    4
    5
    // Good: Pre-allocate capacity
    slice := make([]int, 0, expectedSize)
    
    // Avoid: Multiple reallocations
    var slice []int  // Will grow from 0 capacity
    

  2. Use Full Slice Expression for Safety

    // Limits capacity to prevent accidental overwrites
    safe := original[start:end:end]
    

  3. Copy When You Need Independence

    1
    2
    3
    // Create independent copy
    independent := make([]Type, len(original))
    copy(independent, original)
    

  4. Avoid Memory Leaks with Large Slices

    1
    2
    3
    // Copy small portion instead of keeping reference
    small := make([]Type, smallSize)
    copy(small, large[start:end])
    

Common Pitfalls

  • Slice Header Copying: Slices are reference types, assignments copy the header, not data
  • Capacity Confusion: Length vs capacity - understand the difference
  • Memory Leaks: Small slices can hold references to large underlying arrays
  • Concurrent Access: Slices are not thread-safe without synchronization

Quick Reference 📑

Key Takeaways

  1. Creation: Use literals, make(), or slicing operations
  2. Growth: append() automatically manages capacity
  3. Copying: copy() for safe element transfer
  4. Memory: Understand underlying array sharing
  5. Performance: Pre-allocate capacity when size is known
  6. Safety: Use bounds checking and full slice expressions

Remember

"Slices are the heart of Go collections. Master append, copy, and capacity management for efficient and safe code. When in doubt, copy for independence."