Navigation

Go

Understanding Go Slices vs Arrays: When to Use Which in 2025

Master Go slices and arrays with comprehensive examples. Learn key differences, performance implications, memory management, and when to choose slices vs arrays in Go applications 2025.

Table Of Contents

Introduction

One of the most fundamental concepts in Go programming is understanding the difference between slices and arrays. While they may seem similar on the surface, they have vastly different characteristics that affect performance, memory usage, and how your code behaves. This distinction often confuses developers coming from other languages where these concepts are either merged or handled differently.

Arrays in Go are fixed-size sequences with their size being part of the type, while slices are dynamic, flexible views into arrays. Understanding when to use each can dramatically impact your application's performance and design. Many Go developers default to using slices for everything, but there are specific scenarios where arrays are more appropriate.

In this comprehensive guide, we'll explore the fundamental differences between slices and arrays, examine their internal implementations, and provide practical guidelines for choosing between them. You'll learn about memory implications, performance characteristics, and real-world patterns that will help you write more efficient Go code.

Understanding Arrays in Go

Array Basics

Arrays in Go are fixed-size sequences of elements where the size is part of the type definition:

package main

import "fmt"

func main() {
    // Declaration with explicit size
    var arr1 [5]int
    fmt.Printf("Zero-value array: %v\n", arr1)
    
    // Declaration with initialization
    arr2 := [5]int{1, 2, 3, 4, 5}
    fmt.Printf("Initialized array: %v\n", arr2)
    
    // Let compiler infer size
    arr3 := [...]int{10, 20, 30}
    fmt.Printf("Inferred size array: %v, length: %d\n", arr3, len(arr3))
    
    // Partial initialization
    arr4 := [5]int{1, 2} // remaining elements are zero
    fmt.Printf("Partial init array: %v\n", arr4)
    
    // Array with specific indices
    arr5 := [5]int{0: 10, 2: 30, 4: 50}
    fmt.Printf("Index-specific array: %v\n", arr5)
}

Array Characteristics

Arrays have several important characteristics:

package main

import (
    "fmt"
    "unsafe"
)

func demonstrateArrayCharacteristics() {
    // Size is part of the type
    var arr1 [3]int
    var arr2 [5]int
    
    // These are different types!
    fmt.Printf("Type of arr1: %T\n", arr1)
    fmt.Printf("Type of arr2: %T\n", arr2)
    
    // Arrays are value types - they are copied
    original := [3]int{1, 2, 3}
    copy := original
    copy[0] = 999
    
    fmt.Printf("Original: %v\n", original) // [1 2 3]
    fmt.Printf("Copy: %v\n", copy)         // [999 2 3]
    
    // Memory layout is contiguous
    arr := [4]int{10, 20, 30, 40}
    fmt.Printf("Array: %v\n", arr)
    fmt.Printf("Size in memory: %d bytes\n", unsafe.Sizeof(arr))
    
    // Print memory addresses
    for i := 0; i < len(arr); i++ {
        fmt.Printf("arr[%d] address: %p\n", i, &arr[i])
    }
}

func main() {
    demonstrateArrayCharacteristics()
}

When to Use Arrays

Arrays are best suited for:

package main

import "fmt"

// 1. Fixed-size data structures
type RGB struct {
    values [3]uint8 // Red, Green, Blue
}

func (rgb RGB) String() string {
    return fmt.Sprintf("RGB(%d, %d, %d)", rgb.values[0], rgb.values[1], rgb.values[2])
}

// 2. Mathematical operations
type Vector3D [3]float64

func (v Vector3D) Magnitude() float64 {
    return math.Sqrt(v[0]*v[0] + v[1]*v[1] + v[2]*v[2])
}

func (v1 Vector3D) Add(v2 Vector3D) Vector3D {
    return Vector3D{v1[0] + v2[0], v1[1] + v2[1], v1[2] + v2[2]}
}

// 3. Small, known-size collections
type WeekDays [7]string

func NewWeekDays() WeekDays {
    return WeekDays{
        "Sunday", "Monday", "Tuesday", "Wednesday",
        "Thursday", "Friday", "Saturday",
    }
}

// 4. Performance-critical small buffers
func processSmallBuffer() {
    // For small, fixed-size buffers, arrays can be more efficient
    var buffer [256]byte
    
    // Process data directly in the array
    for i := range buffer {
        buffer[i] = byte(i % 256)
    }
    
    fmt.Printf("Buffer size: %d\n", len(buffer))
}

func main() {
    // RGB color
    red := RGB{values: [3]uint8{255, 0, 0}}
    fmt.Println(red)
    
    // 3D vectors
    v1 := Vector3D{1.0, 2.0, 3.0}
    v2 := Vector3D{4.0, 5.0, 6.0}
    result := v1.Add(v2)
    fmt.Printf("Vector addition: %v\n", result)
    
    // Week days
    days := NewWeekDays()
    fmt.Printf("First day: %s\n", days[0])
    
    processSmallBuffer()
}

Understanding Slices in Go

Slice Basics

Slices are dynamic arrays that provide a more flexible way to work with sequences:

package main

import "fmt"

func main() {
    // Declaration creates nil slice
    var slice1 []int
    fmt.Printf("Nil slice: %v, len: %d, cap: %d\n", slice1, len(slice1), cap(slice1))
    
    // Make creates slice with specified length and capacity
    slice2 := make([]int, 5)      // length 5, capacity 5
    slice3 := make([]int, 3, 10)  // length 3, capacity 10
    
    fmt.Printf("Made slice2: %v, len: %d, cap: %d\n", slice2, len(slice2), cap(slice2))
    fmt.Printf("Made slice3: %v, len: %d, cap: %d\n", slice3, len(slice3), cap(slice3))
    
    // Slice literal
    slice4 := []int{1, 2, 3, 4, 5}
    fmt.Printf("Literal slice: %v, len: %d, cap: %d\n", slice4, len(slice4), cap(slice4))
    
    // Slice from array
    arr := [5]int{10, 20, 30, 40, 50}
    slice5 := arr[1:4]  // Elements at index 1, 2, 3
    fmt.Printf("Slice from array: %v, len: %d, cap: %d\n", slice5, len(slice5), cap(slice5))
}

Slice Internal Structure

Understanding how slices work internally is crucial:

package main

import (
    "fmt"
    "unsafe"
)

// SliceHeader represents the internal structure of a slice
type SliceHeader struct {
    Data uintptr // pointer to the underlying array
    Len  int     // number of elements in the slice
    Cap  int     // capacity of the underlying array
}

func demonstrateSliceInternals() {
    // Create a slice
    slice := []int{1, 2, 3, 4, 5}
    
    // Print slice header information
    header := (*SliceHeader)(unsafe.Pointer(&slice))
    fmt.Printf("Slice: %v\n", slice)
    fmt.Printf("Data pointer: %x\n", header.Data)
    fmt.Printf("Length: %d\n", header.Len)
    fmt.Printf("Capacity: %d\n", header.Cap)
    
    // Create sub-slices
    slice1 := slice[1:3]  // [2, 3]
    slice2 := slice[2:5]  // [3, 4, 5]
    
    fmt.Printf("\nSub-slice 1: %v\n", slice1)
    fmt.Printf("Sub-slice 2: %v\n", slice2)
    
    // Modify original slice
    slice[2] = 999
    
    fmt.Printf("\nAfter modifying original slice[2]:\n")
    fmt.Printf("Original: %v\n", slice)
    fmt.Printf("Sub-slice 1: %v\n", slice1)
    fmt.Printf("Sub-slice 2: %v\n", slice2)
}

func main() {
    demonstrateSliceInternals()
}

Slice Operations

Slices support many operations that arrays don't:

package main

import "fmt"

func demonstrateSliceOperations() {
    // Append operation
    slice := []int{1, 2, 3}
    fmt.Printf("Original: %v, len: %d, cap: %d\n", slice, len(slice), cap(slice))
    
    slice = append(slice, 4, 5, 6)
    fmt.Printf("After append: %v, len: %d, cap: %d\n", slice, len(slice), cap(slice))
    
    // Copy operation
    source := []int{1, 2, 3, 4, 5}
    dest := make([]int, 3)
    copied := copy(dest, source)
    fmt.Printf("Copied %d elements: %v\n", copied, dest)
    
    // Slice expansion (reslicing)
    original := []int{1, 2, 3, 4, 5}
    sub := original[1:3]
    fmt.Printf("Sub-slice: %v, len: %d, cap: %d\n", sub, len(sub), cap(sub))
    
    // Expand the sub-slice within its capacity
    expanded := sub[:4]  // Expand to show elements 2, 3, 4, 5
    fmt.Printf("Expanded: %v, len: %d, cap: %d\n", expanded, len(expanded), cap(expanded))
    
    // Delete operation (using slicing)
    slice = []int{1, 2, 3, 4, 5}
    indexToDelete := 2
    slice = append(slice[:indexToDelete], slice[indexToDelete+1:]...)
    fmt.Printf("After deleting index 2: %v\n", slice)
    
    // Insert operation
    slice = []int{1, 2, 4, 5}
    indexToInsert := 2
    valueToInsert := 3
    slice = append(slice[:indexToInsert], append([]int{valueToInsert}, slice[indexToInsert:]...)...)
    fmt.Printf("After inserting 3 at index 2: %v\n", slice)
}

func main() {
    demonstrateSliceOperations()
}

Performance Comparison

Memory Allocation Patterns

package main

import (
    "fmt"
    "runtime"
    "time"
)

func benchmarkArrayVsSlice() {
    const iterations = 1000000
    
    // Benchmark array allocation
    start := time.Now()
    var m1 runtime.MemStats
    runtime.ReadMemStats(&m1)
    
    for i := 0; i < iterations; i++ {
        arr := [100]int{}
        _ = arr
    }
    
    var m2 runtime.MemStats
    runtime.ReadMemStats(&m2)
    arrayTime := time.Since(start)
    arrayMemory := m2.TotalAlloc - m1.TotalAlloc
    
    // Benchmark slice allocation
    start = time.Now()
    runtime.ReadMemStats(&m1)
    
    for i := 0; i < iterations; i++ {
        slice := make([]int, 100)
        _ = slice
    }
    
    runtime.ReadMemStats(&m2)
    sliceTime := time.Since(start)
    sliceMemory := m2.TotalAlloc - m1.TotalAlloc
    
    fmt.Printf("Array allocation: %v, Memory: %d bytes\n", arrayTime, arrayMemory)
    fmt.Printf("Slice allocation: %v, Memory: %d bytes\n", sliceTime, sliceMemory)
}

func benchmarkPassingArrayVsSlice() {
    const iterations = 100000
    
    // Large array for testing
    largeArray := [1000]int{}
    for i := range largeArray {
        largeArray[i] = i
    }
    
    largeSlice := make([]int, 1000)
    for i := range largeSlice {
        largeSlice[i] = i
    }
    
    // Benchmark passing array by value
    start := time.Now()
    for i := 0; i < iterations; i++ {
        processArray(largeArray)
    }
    arrayTime := time.Since(start)
    
    // Benchmark passing slice
    start = time.Now()
    for i := 0; i < iterations; i++ {
        processSlice(largeSlice)
    }
    sliceTime := time.Since(start)
    
    fmt.Printf("Passing array by value: %v\n", arrayTime)
    fmt.Printf("Passing slice: %v\n", sliceTime)
}

func processArray(arr [1000]int) int {
    sum := 0
    for _, v := range arr {
        sum += v
    }
    return sum
}

func processSlice(slice []int) int {
    sum := 0
    for _, v := range slice {
        sum += v
    }
    return sum
}

func main() {
    fmt.Println("Memory allocation benchmark:")
    benchmarkArrayVsSlice()
    
    fmt.Println("\nPassing parameter benchmark:")
    benchmarkPassingArrayVsSlice()
}

Growth Patterns and Capacity Management

package main

import "fmt"

func demonstrateSliceGrowth() {
    slice := make([]int, 0)
    
    fmt.Println("Slice growth pattern:")
    for i := 0; i < 20; i++ {
        oldCap := cap(slice)
        slice = append(slice, i)
        newCap := cap(slice)
        
        if newCap != oldCap {
            fmt.Printf("Length: %d, Capacity changed: %d -> %d\n", len(slice), oldCap, newCap)
        }
    }
    
    // Pre-allocating for better performance
    fmt.Println("\nPre-allocation vs dynamic growth:")
    
    // Dynamic growth
    start := time.Now()
    var dynamicSlice []int
    for i := 0; i < 10000; i++ {
        dynamicSlice = append(dynamicSlice, i)
    }
    dynamicTime := time.Since(start)
    
    // Pre-allocated
    start = time.Now()
    preAllocSlice := make([]int, 0, 10000)
    for i := 0; i < 10000; i++ {
        preAllocSlice = append(preAllocSlice, i)
    }
    preAllocTime := time.Since(start)
    
    fmt.Printf("Dynamic growth: %v\n", dynamicTime)
    fmt.Printf("Pre-allocated: %v\n", preAllocTime)
}

func main() {
    demonstrateSliceGrowth()
}

Practical Usage Patterns

Pattern 1: Collection Processing

package main

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

// Array-based approach for fixed data
type MonthNames [12]string

func NewMonthNames() MonthNames {
    return MonthNames{
        "January", "February", "March", "April", "May", "June",
        "July", "August", "September", "October", "November", "December",
    }
}

func (m MonthNames) GetMonth(index int) string {
    if index < 1 || index > 12 {
        return "Invalid"
    }
    return m[index-1]
}

// Slice-based approach for dynamic data
type TodoList struct {
    items []string
}

func NewTodoList() *TodoList {
    return &TodoList{
        items: make([]string, 0),
    }
}

func (t *TodoList) Add(item string) {
    t.items = append(t.items, item)
}

func (t *TodoList) Remove(index int) error {
    if index < 0 || index >= len(t.items) {
        return fmt.Errorf("index %d out of range", index)
    }
    t.items = append(t.items[:index], t.items[index+1:]...)
    return nil
}

func (t *TodoList) Filter(predicate func(string) bool) []string {
    var result []string
    for _, item := range t.items {
        if predicate(item) {
            result = append(result, item)
        }
    }
    return result
}

func (t *TodoList) Sort() {
    sort.Strings(t.items)
}

func (t *TodoList) Items() []string {
    // Return a copy to prevent external modification
    result := make([]string, len(t.items))
    copy(result, t.items)
    return result
}

func main() {
    // Fixed-size data with arrays
    months := NewMonthNames()
    fmt.Printf("Month 3: %s\n", months.GetMonth(3))
    fmt.Printf("Month 13: %s\n", months.GetMonth(13))
    
    // Dynamic data with slices
    todo := NewTodoList()
    todo.Add("Buy groceries")
    todo.Add("Walk the dog")
    todo.Add("Finish project")
    todo.Add("Call mom")
    
    fmt.Printf("All items: %v\n", todo.Items())
    
    // Filter items containing "the"
    filtered := todo.Filter(func(item string) bool {
        return strings.Contains(item, "the")
    })
    fmt.Printf("Items containing 'the': %v\n", filtered)
    
    todo.Sort()
    fmt.Printf("Sorted items: %v\n", todo.Items())
    
    todo.Remove(1)
    fmt.Printf("After removing index 1: %v\n", todo.Items())
}

Pattern 2: Buffer Management

package main

import (
    "fmt"
    "io"
)

// Array-based buffer for small, fixed-size operations
type SmallBuffer struct {
    data [256]byte
    pos  int
}

func (b *SmallBuffer) Write(p []byte) (n int, err error) {
    available := len(b.data) - b.pos
    if len(p) > available {
        return 0, fmt.Errorf("buffer overflow")
    }
    
    copy(b.data[b.pos:], p)
    b.pos += len(p)
    return len(p), nil
}

func (b *SmallBuffer) Bytes() []byte {
    return b.data[:b.pos]
}

func (b *SmallBuffer) Reset() {
    b.pos = 0
}

// Slice-based buffer for dynamic operations
type DynamicBuffer struct {
    data []byte
}

func NewDynamicBuffer(initialCapacity int) *DynamicBuffer {
    return &DynamicBuffer{
        data: make([]byte, 0, initialCapacity),
    }
}

func (b *DynamicBuffer) Write(p []byte) (n int, err error) {
    b.data = append(b.data, p...)
    return len(p), nil
}

func (b *DynamicBuffer) WriteByte(c byte) error {
    b.data = append(b.data, c)
    return nil
}

func (b *DynamicBuffer) WriteString(s string) (n int, err error) {
    return b.Write([]byte(s))
}

func (b *DynamicBuffer) Bytes() []byte {
    return b.data
}

func (b *DynamicBuffer) String() string {
    return string(b.data)
}

func (b *DynamicBuffer) Reset() {
    b.data = b.data[:0] // Keep capacity
}

func (b *DynamicBuffer) Grow(n int) {
    if cap(b.data)-len(b.data) < n {
        newCap := len(b.data) + n
        if newCap < 2*cap(b.data) {
            newCap = 2 * cap(b.data)
        }
        newData := make([]byte, len(b.data), newCap)
        copy(newData, b.data)
        b.data = newData
    }
}

func demonstrateBuffers() {
    // Small buffer for fixed-size operations
    small := &SmallBuffer{}
    small.Write([]byte("Hello, "))
    small.Write([]byte("World!"))
    fmt.Printf("Small buffer: %s\n", small.Bytes())
    
    // Dynamic buffer for variable-size operations
    dynamic := NewDynamicBuffer(10)
    dynamic.WriteString("This is a longer message ")
    dynamic.WriteString("that demonstrates dynamic ")
    dynamic.WriteString("buffer growth capabilities.")
    
    fmt.Printf("Dynamic buffer: %s\n", dynamic.String())
    fmt.Printf("Dynamic buffer capacity: %d\n", cap(dynamic.data))
}

func main() {
    demonstrateBuffers()
}

Pattern 3: Matrix Operations

package main

import "fmt"

// Array-based approach for small, fixed matrices
type Matrix3x3 [3][3]float64

func (m Matrix3x3) Multiply(other Matrix3x3) Matrix3x3 {
    var result Matrix3x3
    
    for i := 0; i < 3; i++ {
        for j := 0; j < 3; j++ {
            for k := 0; k < 3; k++ {
                result[i][j] += m[i][k] * other[k][j]
            }
        }
    }
    
    return result
}

func (m Matrix3x3) String() string {
    result := ""
    for i := 0; i < 3; i++ {
        result += fmt.Sprintf("[%6.2f %6.2f %6.2f]\n", m[i][0], m[i][1], m[i][2])
    }
    return result
}

// Slice-based approach for dynamic matrices
type Matrix struct {
    data [][]float64
    rows int
    cols int
}

func NewMatrix(rows, cols int) *Matrix {
    data := make([][]float64, rows)
    for i := range data {
        data[i] = make([]float64, cols)
    }
    
    return &Matrix{
        data: data,
        rows: rows,
        cols: cols,
    }
}

func (m *Matrix) Set(row, col int, value float64) error {
    if row < 0 || row >= m.rows || col < 0 || col >= m.cols {
        return fmt.Errorf("index out of bounds")
    }
    m.data[row][col] = value
    return nil
}

func (m *Matrix) Get(row, col int) (float64, error) {
    if row < 0 || row >= m.rows || col < 0 || col >= m.cols {
        return 0, fmt.Errorf("index out of bounds")
    }
    return m.data[row][col], nil
}

func (m *Matrix) Multiply(other *Matrix) (*Matrix, error) {
    if m.cols != other.rows {
        return nil, fmt.Errorf("incompatible matrix dimensions")
    }
    
    result := NewMatrix(m.rows, other.cols)
    
    for i := 0; i < m.rows; i++ {
        for j := 0; j < other.cols; j++ {
            sum := 0.0
            for k := 0; k < m.cols; k++ {
                sum += m.data[i][k] * other.data[k][j]
            }
            result.data[i][j] = sum
        }
    }
    
    return result, nil
}

func (m *Matrix) String() string {
    result := ""
    for i := 0; i < m.rows; i++ {
        result += "["
        for j := 0; j < m.cols; j++ {
            result += fmt.Sprintf("%6.2f", m.data[i][j])
            if j < m.cols-1 {
                result += " "
            }
        }
        result += "]\n"
    }
    return result
}

func demonstrateMatrices() {
    // Fixed 3x3 matrices
    m1 := Matrix3x3{
        {1, 2, 3},
        {4, 5, 6},
        {7, 8, 9},
    }
    
    m2 := Matrix3x3{
        {9, 8, 7},
        {6, 5, 4},
        {3, 2, 1},
    }
    
    result3x3 := m1.Multiply(m2)
    fmt.Println("3x3 Matrix multiplication:")
    fmt.Println("Matrix 1:")
    fmt.Print(m1)
    fmt.Println("Matrix 2:")
    fmt.Print(m2)
    fmt.Println("Result:")
    fmt.Print(result3x3)
    
    // Dynamic matrices
    dynamic1 := NewMatrix(2, 3)
    dynamic1.Set(0, 0, 1)
    dynamic1.Set(0, 1, 2)
    dynamic1.Set(0, 2, 3)
    dynamic1.Set(1, 0, 4)
    dynamic1.Set(1, 1, 5)
    dynamic1.Set(1, 2, 6)
    
    dynamic2 := NewMatrix(3, 2)
    dynamic2.Set(0, 0, 7)
    dynamic2.Set(0, 1, 8)
    dynamic2.Set(1, 0, 9)
    dynamic2.Set(1, 1, 10)
    dynamic2.Set(2, 0, 11)
    dynamic2.Set(2, 1, 12)
    
    dynamicResult, err := dynamic1.Multiply(dynamic2)
    if err != nil {
        fmt.Printf("Error: %v\n", err)
        return
    }
    
    fmt.Println("\nDynamic matrix multiplication:")
    fmt.Println("Matrix 1 (2x3):")
    fmt.Print(dynamic1)
    fmt.Println("Matrix 2 (3x2):")
    fmt.Print(dynamic2)
    fmt.Println("Result (2x2):")
    fmt.Print(dynamicResult)
}

func main() {
    demonstrateMatrices()
}

Common Pitfalls and Best Practices

Pitfall 1: Slice Reference Issues

package main

import "fmt"

// Bad: Returning slice of local array
func badCreateSlice() []int {
    arr := [5]int{1, 2, 3, 4, 5}
    return arr[:] // This creates a slice that references the array
}

// Good: Return copy or use make
func goodCreateSlice() []int {
    data := []int{1, 2, 3, 4, 5}
    result := make([]int, len(data))
    copy(result, data)
    return result
}

// Pitfall: Modifying slice affects sub-slices
func demonstrateSliceReferences() {
    original := []int{1, 2, 3, 4, 5, 6}
    slice1 := original[1:4] // [2, 3, 4]
    slice2 := original[2:5] // [3, 4, 5]
    
    fmt.Printf("Original: %v\n", original)
    fmt.Printf("Slice1: %v\n", slice1)
    fmt.Printf("Slice2: %v\n", slice2)
    
    // Modify through slice1
    slice1[1] = 999 // This modifies original[2]
    
    fmt.Printf("\nAfter modifying slice1[1] = 999:\n")
    fmt.Printf("Original: %v\n", original)
    fmt.Printf("Slice1: %v\n", slice1)
    fmt.Printf("Slice2: %v\n", slice2) // slice2[0] is also affected!
}

// Solution: Create independent copies when needed
func createIndependentSlice(source []int, start, end int) []int {
    result := make([]int, end-start)
    copy(result, source[start:end])
    return result
}

func main() {
    demonstrateSliceReferences()
    
    fmt.Println("\nCreating independent slices:")
    original := []int{1, 2, 3, 4, 5, 6}
    independent := createIndependentSlice(original, 1, 4)
    
    fmt.Printf("Original: %v\n", original)
    fmt.Printf("Independent: %v\n", independent)
    
    independent[1] = 999
    fmt.Printf("After modifying independent slice:\n")
    fmt.Printf("Original: %v\n", original)
    fmt.Printf("Independent: %v\n", independent)
}

Pitfall 2: Memory Leaks with Large Slices

package main

import (
    "fmt"
    "runtime"
)

// Bad: Keeping reference to large slice
func badProcessLargeData() []int {
    largeData := make([]int, 1000000)
    for i := range largeData {
        largeData[i] = i
    }
    
    // Return only first 10 elements but keep reference to entire array
    return largeData[:10]
}

// Good: Create independent small slice
func goodProcessLargeData() []int {
    largeData := make([]int, 1000000)
    for i := range largeData {
        largeData[i] = i
    }
    
    // Create independent slice with only needed data
    result := make([]int, 10)
    copy(result, largeData[:10])
    return result
}

func demonstrateMemoryLeak() {
    var m1, m2 runtime.MemStats
    
    runtime.GC()
    runtime.ReadMemStats(&m1)
    
    // This keeps the large array in memory
    badResult := badProcessLargeData()
    
    runtime.GC()
    runtime.ReadMemStats(&m2)
    
    fmt.Printf("Bad approach - Memory used: %d KB\n", (m2.Alloc-m1.Alloc)/1024)
    fmt.Printf("Bad result: %v\n", badResult[:5])
    
    runtime.ReadMemStats(&m1)
    
    // This allows the large array to be garbage collected
    goodResult := goodProcessLargeData()
    
    runtime.GC()
    runtime.ReadMemStats(&m2)
    
    fmt.Printf("Good approach - Memory used: %d KB\n", (m2.Alloc-m1.Alloc)/1024)
    fmt.Printf("Good result: %v\n", goodResult[:5])
}

func main() {
    demonstrateMemoryLeak()
}

Advanced Techniques

Zero-Copy Slice Operations

package main

import "fmt"

// Efficient slice operations that minimize allocations
type SliceUtils struct{}

// Efficient removal without preserving order
func (SliceUtils) RemoveFast(slice []int, index int) []int {
    if index < 0 || index >= len(slice) {
        return slice
    }
    
    // Move last element to the position and truncate
    slice[index] = slice[len(slice)-1]
    return slice[:len(slice)-1]
}

// Efficient removal preserving order
func (SliceUtils) RemoveOrdered(slice []int, index int) []int {
    if index < 0 || index >= len(slice) {
        return slice
    }
    
    return append(slice[:index], slice[index+1:]...)
}

// Efficient filtering in-place
func (SliceUtils) FilterInPlace(slice []int, predicate func(int) bool) []int {
    writeIndex := 0
    
    for readIndex, value := range slice {
        if predicate(value) {
            if writeIndex != readIndex {
                slice[writeIndex] = value
            }
            writeIndex++
        }
    }
    
    return slice[:writeIndex]
}

// Efficient deduplication
func (SliceUtils) DeduplicateOrdered(slice []int) []int {
    if len(slice) <= 1 {
        return slice
    }
    
    writeIndex := 1
    
    for readIndex := 1; readIndex < len(slice); readIndex++ {
        if slice[readIndex] != slice[readIndex-1] {
            if writeIndex != readIndex {
                slice[writeIndex] = slice[readIndex]
            }
            writeIndex++
        }
    }
    
    return slice[:writeIndex]
}

func demonstrateZeroCopyOperations() {
    utils := SliceUtils{}
    
    // Fast removal
    slice1 := []int{1, 2, 3, 4, 5}
    slice1 = utils.RemoveFast(slice1, 2)
    fmt.Printf("After fast removal: %v\n", slice1)
    
    // Ordered removal
    slice2 := []int{1, 2, 3, 4, 5}
    slice2 = utils.RemoveOrdered(slice2, 2)
    fmt.Printf("After ordered removal: %v\n", slice2)
    
    // In-place filtering
    slice3 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    slice3 = utils.FilterInPlace(slice3, func(x int) bool { return x%2 == 0 })
    fmt.Printf("After filtering even numbers: %v\n", slice3)
    
    // Deduplication
    slice4 := []int{1, 1, 2, 2, 2, 3, 4, 4, 5}
    slice4 = utils.DeduplicateOrdered(slice4)
    fmt.Printf("After deduplication: %v\n", slice4)
}

func main() {
    demonstrateZeroCopyOperations()
}

FAQ

Q: When should I use arrays instead of slices? A: Use arrays for fixed-size data structures, small collections where size is known at compile time, mathematical operations with fixed dimensions, and when you want value semantics (copying behavior).

Q: Do slices have better performance than arrays? A: Not necessarily. Arrays can be more efficient for small, fixed-size data due to stack allocation and better cache locality. Slices are more efficient when you need dynamic sizing or want to avoid copying large amounts of data.

Q: Can I convert an array to a slice? A: Yes, use the slice operator: arr[:] creates a slice that references the entire array, or arr[start:end] for a portion of the array.

Q: What happens when a slice grows beyond its capacity? A: Go allocates a new underlying array (typically with double the capacity), copies all elements to the new array, and updates the slice to point to the new array. The old array becomes eligible for garbage collection.

Q: How do I avoid memory leaks when working with large slices? A: When you only need a small portion of a large slice, create an independent copy using make() and copy() instead of sub-slicing, which keeps the entire underlying array in memory.

Q: Are multi-dimensional arrays and slices different? A: Yes, multi-dimensional arrays like [3][4]int are truly two-dimensional with contiguous memory. Slice of slices like [][]int is a slice where each element is itself a slice, potentially with different lengths and non-contiguous memory.

Conclusion

Understanding the differences between Go arrays and slices is fundamental to writing efficient Go code. The key takeaways are:

  • Arrays are fixed-size value types best suited for small, known-size collections and mathematical operations
  • Slices are dynamic reference types ideal for collections that change size and when you need flexible data manipulation
  • Performance characteristics differ: arrays can be more efficient for small data, slices for large data and dynamic operations
  • Memory management considerations: be aware of slice growth patterns and potential memory leaks with sub-slices
  • Choose based on your use case: fixed size and value semantics favor arrays, dynamic operations favor slices

By mastering these concepts, you'll be able to make informed decisions about data structure choices in your Go applications, leading to better performance and more maintainable code.

Ready to optimize your Go data structures? Start by reviewing your current code for opportunities to use the most appropriate type for each use case. Benchmark critical sections to measure the actual performance impact of your choices. Share your experiences and questions in the comments below!

Share this article

Add Comment

No comments yet. Be the first to comment!

More from Go