Back to blog

Learning Go: Never Use Arrays

GoBest PracticesArraysSlicesData Structures

Introduction

If you're new to Go, you might be surprised to learn that arrays are rarely used in idiomatic Go code. Despite being a fundamental data structure in most programming languages, Go arrays have significant limitations that make them impractical for most real-world scenarios.

In this article, we'll explore why arrays should almost never be used in Go, understand the critical differences between arrays and slices, and learn when slices are the better choice (which is nearly always).

What Are Arrays in Go?

In Go, an array is a fixed-size, numbered sequence of elements of a single type. The size is part of the array's type, which leads to some surprising limitations.

package main
 
import "fmt"
 
func main() {
    // Array declaration - size is part of the type
    var numbers [5]int
    numbers[0] = 10
    numbers[1] = 20
    
    // Array literal
    fruits := [3]string{"apple", "banana", "orange"}
    
    fmt.Println(numbers) // [10 20 0 0 0]
    fmt.Println(fruits)  // [apple banana orange]
}

Seems straightforward, right? Let's see why this becomes problematic.

The Fundamental Problem: Size is Part of the Type

The most critical issue with Go arrays is that the size is part of the type definition. This means [3]int and [5]int are completely different, incompatible types.

package main
 
import "fmt"
 
func printArray(arr [3]int) {
    fmt.Println(arr)
}
 
func main() {
    arr1 := [3]int{1, 2, 3}
    arr2 := [5]int{1, 2, 3, 4, 5}
    
    printArray(arr1) // ✅ Works
    // printArray(arr2) // ❌ Compile error: cannot use arr2 (type [5]int) as type [3]int
}

Problem: You can't write generic functions that work with arrays of different sizes. You'd need a separate function for every array size!

Problem #1: Arrays Are Passed By Value

In Go, arrays are value types, not reference types. When you pass an array to a function, the entire array is copied.

package main
 
import "fmt"
 
func modifyArray(arr [5]int) {
    arr[0] = 999
    fmt.Println("Inside function:", arr)
}
 
func main() {
    numbers := [5]int{1, 2, 3, 4, 5}
    modifyArray(numbers)
    
    fmt.Println("Original array:", numbers)
}

Output:

Inside function: [999 2 3 4 5]
Original array: [1 2 3 4 5]

Problem: The modification didn't affect the original array because a full copy was made. For large arrays, this is both inefficient (memory waste) and confusing (unexpected behavior).

Workaround: Passing Pointers (Still Not Great)

func modifyArray(arr *[5]int) {
    arr[0] = 999
}
 
func main() {
    numbers := [5]int{1, 2, 3, 4, 5}
    modifyArray(&numbers)
    
    fmt.Println(numbers) // [999 2 3 4 5]
}

This works, but now you're stuck with the fixed size problem again. You need different functions for *[5]int, *[10]int, etc.

Problem #2: No Built-in Growth

Arrays have a fixed size. You cannot append elements or resize them.

func main() {
    arr := [3]int{1, 2, 3}
    
    // arr = append(arr, 4) // ❌ Compile error: first argument to append must be slice
    
    // To "add" an element, you'd need to create a new array
    newArr := [4]int{arr[0], arr[1], arr[2], 4}
    fmt.Println(newArr)
}

Problem: This is tedious, error-prone, and inefficient.

Problem #3: Cannot Return Different Sizes

Since array size is part of the type, functions must return a specific size.

// This function can ONLY return arrays of size 3
func getNumbers() [3]int {
    return [3]int{1, 2, 3}
}
 
// You can't write a function that returns arrays of dynamic size
// func getDynamicArray(size int) [size]int { // ❌ Invalid syntax
//     ...
// }

Problem: You can't write flexible functions that return different-sized collections.

Problem #4: Incompatible with Standard Library

Most of Go's standard library functions work with slices, not arrays.

import (
    "fmt"
    "sort"
)
 
func main() {
    arr := [5]int{5, 2, 8, 1, 9}
    
    // sort.Ints(arr) // ❌ Compile error: cannot use arr (type [5]int) as type []int
    
    // You'd need to convert to a slice
    sort.Ints(arr[:])
    fmt.Println(arr)
}

Problem: Arrays don't integrate well with the Go ecosystem.

The Solution: Use Slices

Slices are the answer to all these problems. A slice is a flexible, dynamic view into an array.

package main
 
import "fmt"
 
func main() {
    // Slice declaration (no size specified)
    var numbers []int
    
    // Slice literal
    fruits := []string{"apple", "banana", "orange"}
    
    // Appending to a slice
    numbers = append(numbers, 1, 2, 3)
    
    fmt.Println(numbers) // [1 2 3]
    fmt.Println(fruits)  // [apple banana orange]
}

Slices vs Arrays: Key Differences

FeatureArraySlice
SizeFixed, part of typeDynamic, not part of type
Type[N]Type[]Type
PassingCopied by valueReference to underlying array
ResizableNoYes (with append)
Standard libLimited supportFull support
MemoryValue typeReference type (header + array)

Why Slices Are Better

1. Generic Functions

package main
 
import "fmt"
 
// Works with slices of ANY length
func printSlice(s []int) {
    fmt.Println(s)
}
 
func main() {
    slice1 := []int{1, 2, 3}
    slice2 := []int{1, 2, 3, 4, 5, 6, 7, 8}
    
    printSlice(slice1) // ✅ Works
    printSlice(slice2) // ✅ Works
}

2. Pass by Reference (Lightweight)

package main
 
import "fmt"
 
func modifySlice(s []int) {
    s[0] = 999
    fmt.Println("Inside function:", s)
}
 
func main() {
    numbers := []int{1, 2, 3, 4, 5}
    modifySlice(numbers)
    
    fmt.Println("Original slice:", numbers) // [999 2 3 4 5]
}

Output:

Inside function: [999 2 3 4 5]
Original slice: [999 2 3 4 5]

The slice is passed by reference (actually, a copy of the slice header), so modifications affect the original.

3. Dynamic Growth with append

package main
 
import "fmt"
 
func main() {
    slice := []int{1, 2, 3}
    fmt.Printf("Length: %d, Capacity: %d\n", len(slice), cap(slice))
    
    slice = append(slice, 4, 5)
    fmt.Printf("Length: %d, Capacity: %d\n", len(slice), cap(slice))
    
    fmt.Println(slice) // [1 2 3 4 5]
}

Output:

Length: 3, Capacity: 3
Length: 5, Capacity: 6
[1 2 3 4 5]

Slices automatically grow as needed!

4. Built-in Functions and Standard Library

package main
 
import (
    "fmt"
    "sort"
    "strings"
)
 
func main() {
    // Sorting
    numbers := []int{5, 2, 8, 1, 9}
    sort.Ints(numbers)
    fmt.Println(numbers) // [1 2 5 8 9]
    
    // Strings
    words := []string{"banana", "apple", "cherry"}
    sort.Strings(words)
    fmt.Println(words) // [apple banana cherry]
    
    // Joining
    result := strings.Join(words, ", ")
    fmt.Println(result) // apple, banana, cherry
}

5. Flexible Return Types

package main
 
import "fmt"
 
func getNumbers(count int) []int {
    result := make([]int, count)
    for i := 0; i < count; i++ {
        result[i] = i + 1
    }
    return result
}
 
func main() {
    fmt.Println(getNumbers(3))  // [1 2 3]
    fmt.Println(getNumbers(7))  // [1 2 3 4 5 6 7]
}

Understanding Slice Internals

A slice is a lightweight structure with three components:

type slice struct {
    ptr *Type  // Pointer to underlying array
    len int    // Current length
    cap int    // Capacity (size of underlying array)
}
package main
 
import "fmt"
 
func main() {
    // Create a slice from an array
    arr := [5]int{1, 2, 3, 4, 5}
    slice := arr[1:4] // slice points to arr[1], arr[2], arr[3]
    
    fmt.Println("Array:", arr)
    fmt.Println("Slice:", slice)
    fmt.Printf("Len: %d, Cap: %d\n", len(slice), cap(slice))
    
    // Modifying slice affects the array
    slice[0] = 999
    fmt.Println("Array after modification:", arr)
}

Output:

Array: [1 2 3 4 5]
Slice: [2 3 4]
Len: 3, Cap: 4
Array after modification: [1 999 3 4 5]

When Would You Actually Use Arrays?

There are rare cases where arrays make sense:

1. Fixed-Size Data Structures

When the size is truly fixed and meaningful to the type.

type IPv4 [4]byte    // IP address is always 4 bytes
type SHA256 [32]byte // SHA-256 hash is always 32 bytes
type Matrix3x3 [3][3]float64 // 3x3 matrix
 
func parseIP(ip string) IPv4 {
    // ...
}

2. Performance-Critical Code with Known Size

If you need to avoid heap allocations in performance-critical paths.

func process() {
    // Array allocated on stack (faster)
    var buffer [1024]byte
    
    // vs slice which might allocate on heap
    // buffer := make([]byte, 1024)
}

3. Memory Layout Requirements

When you need precise memory layout for system-level programming or CGO.

// For interfacing with C code
type CStruct struct {
    Values [10]uint32 // Must be exactly 10 elements
}

Rule of thumb: If you're not sure whether to use an array or slice, use a slice.

Common Slice Patterns

Creating Slices

// Nil slice
var s []int
 
// Empty slice with make
s = make([]int, 0)
 
// Slice with initial length
s = make([]int, 5) // [0 0 0 0 0]
 
// Slice with length and capacity
s = make([]int, 5, 10) // len=5, cap=10
 
// Slice literal
s = []int{1, 2, 3, 4, 5}

Appending Elements

slice := []int{1, 2, 3}
 
// Append single element
slice = append(slice, 4)
 
// Append multiple elements
slice = append(slice, 5, 6, 7)
 
// Append another slice
other := []int{8, 9}
slice = append(slice, other...) // Note the ...

Copying Slices

original := []int{1, 2, 3}
 
// Create a copy
copied := make([]int, len(original))
copy(copied, original)
 
copied[0] = 999
 
fmt.Println("Original:", original) // [1 2 3]
fmt.Println("Copied:", copied)     // [999 2 3]

Slicing Slices

numbers := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
 
fmt.Println(numbers[2:5])   // [2 3 4]
fmt.Println(numbers[:3])    // [0 1 2]
fmt.Println(numbers[7:])    // [7 8 9]
fmt.Println(numbers[:])     // [0 1 2 3 4 5 6 7 8 9]

Removing Elements

func remove(slice []int, index int) []int {
    return append(slice[:index], slice[index+1:]...)
}
 
numbers := []int{1, 2, 3, 4, 5}
numbers = remove(numbers, 2) // Remove index 2 (value 3)
fmt.Println(numbers) // [1 2 4 5]

Best Practices

1. Always Use Slices for Function Parameters

// Good
func ProcessItems(items []string) {
    // ...
}
 
// Bad
func ProcessItems(items [10]string) {
    // ...
}

2. Pre-allocate Slices When Size is Known

// Efficient
items := make([]int, 0, expectedSize)
for i := 0; i < expectedSize; i++ {
    items = append(items, i)
}
 
// Less efficient (multiple reallocations)
var items []int
for i := 0; i < 1000; i++ {
    items = append(items, i)
}

3. Understand Slice Sharing

original := []int{1, 2, 3, 4, 5}
slice1 := original[1:3]
slice2 := original[2:4]
 
slice1[1] = 999
 
fmt.Println(original) // [1 2 999 4 5]
fmt.Println(slice1)   // [2 999]
fmt.Println(slice2)   // [999 4]

All three slices share the same underlying array!

4. Use copy to Avoid Unintended Sharing

original := []int{1, 2, 3, 4, 5}
independent := make([]int, len(original))
copy(independent, original)
 
independent[0] = 999
 
fmt.Println(original)    // [1 2 3 4 5]
fmt.Println(independent) // [999 2 3 4 5]

5. Check nil vs Empty Slices

var nilSlice []int
emptySlice := []int{}
madeSlice := make([]int, 0)
 
fmt.Println(nilSlice == nil)   // true
fmt.Println(emptySlice == nil) // false
fmt.Println(madeSlice == nil)  // false
 
// But all have length 0
fmt.Println(len(nilSlice))   // 0
fmt.Println(len(emptySlice)) // 0
fmt.Println(len(madeSlice))  // 0

In most cases, treat them the same. Some APIs distinguish between nil and empty slices (like JSON marshaling).

Converting Between Arrays and Slices

Array to Slice

arr := [5]int{1, 2, 3, 4, 5}
 
// Full array as slice
slice := arr[:]
 
// Partial array as slice
slice = arr[1:4]

Slice to Array (Go 1.17+)

slice := []int{1, 2, 3, 4, 5}
 
// Convert to array pointer
arr := (*[5]int)(slice)
 
// Or using type conversion (Go 1.20+)
arr2 := [5]int(slice)

Warning: This panics if the slice length doesn't match the array size!

Performance Considerations

Stack vs Heap Allocation

// Array on stack (fast)
func useArray() {
    var arr [1000]int
    // Use arr...
}
 
// Slice might allocate on heap
func useSlice() {
    slice := make([]int, 1000)
    // Use slice...
}

However, the flexibility of slices almost always outweighs the marginal performance difference. Optimize only if profiling shows it's a bottleneck.

Capacity Management

// Efficient: pre-allocated capacity
slice := make([]int, 0, 1000)
for i := 0; i < 1000; i++ {
    slice = append(slice, i) // No reallocation
}
 
// Less efficient: grows dynamically
var slice []int
for i := 0; i < 1000; i++ {
    slice = append(slice, i) // Multiple reallocations
}

Common Pitfalls

1. Appending to Shared Slices

original := []int{1, 2, 3}
slice1 := original
slice2 := original
 
slice1 = append(slice1, 4)
slice2 = append(slice2, 5)
 
fmt.Println(original) // Might be [1 2 3] or [1 2 3 4] depending on capacity
fmt.Println(slice1)   // [1 2 3 4]
fmt.Println(slice2)   // [1 2 3 5]

Solution: Always reassign the result of append.

2. Loop Variable Addresses

// Wrong
items := []int{1, 2, 3}
var pointers []*int
 
for _, item := range items {
    pointers = append(pointers, &item) // ❌ All point to same variable!
}
 
// All pointers point to the same address (last value)
for _, p := range pointers {
    fmt.Println(*p) // 3, 3, 3
}
 
// Correct
for i := range items {
    pointers = append(pointers, &items[i]) // ✅ Point to slice elements
}

Conclusion

In Go, arrays should almost never be used for general-purpose programming. Here's why:

  • Size is part of the type: Makes generic code impossible
  • Pass by value: Inefficient and confusing
  • No growth: Cannot append or resize
  • Poor ecosystem integration: Standard library uses slices

Use slices instead because they offer:

  • ✅ Flexibility with dynamic sizing
  • ✅ Efficient pass-by-reference semantics
  • ✅ Built-in growth with append
  • ✅ Full standard library support
  • ✅ Cleaner, more maintainable code

Remember: The only time to use arrays is when you have a legitimate reason—fixed-size data structures with meaningful size constraints, specific performance requirements, or system-level programming needs. When in doubt, always use slices.


Go Learning Roadmap:

Other Go Posts:


Further Reading

📬 Subscribe to Newsletter

Get the latest blog posts delivered to your inbox every week. No spam, unsubscribe anytime.

We respect your privacy. Unsubscribe at any time.

💬 Comments

Sign in to leave a comment

We'll never post without your permission.