4.1 Go Arrays vs Slices: Pick Your Container

4.1 Go Arrays vs Slices: Pick Your Container

TL;DR

  • Arrays have a fixed size you can never change. Think of them as a brick.
  • Slices are flexible and can grow or shrink. They are a view or a "slice" of an underlying array.
  • When you add items to a slice and it gets full, Go automatically creates a bigger array and copies everything over for you.
  • For beginners (and most of the time for experts, too), you should almost always use a slice.

Why This Matters

In Go, you'll constantly need to handle lists of things—usernames, prices, messages, you name it. Arrays and slices are the two main tools for this job. Picking the wrong one can lead to rigid code that's hard to change, or to bugs that are tricky to spot. Understanding the difference is a core skill that makes your code simpler and more efficient. Let's make it clear once and for all.


Concepts in Plain English

Before we dive in, let's get our terms straight with some simple analogies.

  • Array: An egg carton. It has a fixed number of spots (e.g., 12), and you can't add a 13th egg without getting a new carton. Its size is part of its identity.
  • Slice: A resizable shopping list. You can start with three items and easily add a fourth, a fifth, and so on. The list itself is just a reference to the actual items.
  • Length (len): The number of items currently in the container. For our shopping list, it's how many items you've written down so far.
  • Capacity (cap): The total space the container has before it needs to resize. For our shopping list, it's the size of the paper. Once it's full, you need to grab a bigger piece of paper.
  • Append: The act of adding a new item to the end of a slice.

Here's a quick glossary to keep handy.

TermSimple DefinitionWhere It Shows Up
ArrayA fixed-size list of elements of the same type.var numbers [3]int
SliceA flexible, dynamic view into an underlying array.var scores []int
Length (len)The number of elements currently in the slice or array.len(mySlice)
Capacity (cap)The size of the underlying array that the slice points to.cap(mySlice)
appendThe built-in function to add elements to a slice.mySlice = append(mySlice, 99)

Do It Step by Step

Let's see how these two work in practice. The only setup you need is to have Go installed on your machine.

1. Making a Rigid Array

Our goal is to create a list that can hold exactly three numbers.

Check the output: You'll see the array and its length.

Array: [100 200 300]
Array length: 3
Pro tip — If you try to access prices[3], Go will stop you with an error because the valid positions (indexes) are 0, 1, and 2. The box simply doesn't exist.

Run it from your terminal:

go run main.go

Declare an array of size 3. The size [3] is a permanent part of its type.

// main.go
package main

import "fmt"

func main() {
    // This array can ONLY hold 3 integers.
    var prices [3]int
    prices[0] = 100
    prices[1] = 200
    prices[2] = 300

    fmt.Println("Array:", prices)
    fmt.Println("Array length:", len(prices))
}

2. Making a Flexible Slice

Now, let's create a list that can grow.

Check the output:

Slice: [88 92 77]
Slice length: 3
Slice capacity: 3

Right now, its length and capacity are the same because it's perfectly full. But that's about to change.

Run it:

go run main.go

Declare a slice. Notice the empty brackets [], which tells Go this is a slice, not an array.

// main.go
package main

import "fmt"

func main() {
    // This slice starts with 3 integers, but it can grow!
    scores := []int{88, 92, 77}

    fmt.Println("Slice:", scores)
    fmt.Println("Slice length:", len(scores))
    fmt.Println("Slice capacity:", cap(scores))
}

3. Growing a Slice with append

This is where the magic happens. Let's add a new score to our slice.

Run it and see the change:

Start: len=3, cap=3, data=[88 92 77]
End:   len=4, cap=6, data=[88 92 77 100]

Add the code to your main function:

// main.go
package main

import "fmt"

func main() {
    scores := []int{88, 92, 77}
    fmt.Printf("Start: len=%d, cap=%d, data=%v\n", len(scores), cap(scores), scores)

    // Add a new score.
    scores = append(scores, 100) // This might trigger a resize!

    fmt.Printf("End:   len=%d, cap=%d, data=%v\n", len(scores), cap(scores), scores)
}

Use the append function. The append function takes a slice and the new element(s) you want to add. It returns a new slice.

Warning — This is crucial: you must save the result of append back to your original slice variable, like scores = append(...). If you don't, you'll lose the new item.

What happened? The original slice had a capacity of 3. When we tried to add a 4th item, Go did this behind the scenes:

  1. Realized the underlying array was full.
  2. Created a brand new, bigger array (in this case, with a capacity of 6).
  3. Copied the old values (88, 92, 77) into the new array.
  4. Added the new value (100).
  5. Pointed our scores slice to this new array.

This process is summarized in the table below.

Actionscores variable containsLengthCapacityWhat's Happening Under the Hood
scores := []int{88, 92, 77}[88 92 77]33Go creates an array of size 3 to hold the data.
scores = append(scores, 100)[88 92 77 100]46The old array was full. Go allocated a new, bigger array (size 6), copied the old data, and added the new item.

Examples Section

Example A: A Simple To-Do List

This is the most common way you'll use slices: create an empty one and add items as you go.

// main.go
package main

import "fmt"

func main() {
    // Start with an empty list of tasks.
    var tasks []string

    // Add items one by one.
    tasks = append(tasks, "Buy milk")
    tasks = append(tasks, "Read a book")
    tasks = append(tasks, "Call mom")

    fmt.Println("My To-Do List:")
    for i, task := range tasks {
        fmt.Printf("%d. %s\n", i+1, task)
    }
}

Output:

My To-Do List:
1. Buy milk
2. Read a book
3. Call mom

Example B: A Slice as a "View"

This example shows that a slice is just a window into an array. Changing the slice can change the original array.

package main

import "fmt"

func main() {
    // Start with a fixed array of numbers.
    originalArray := [5]string{"a", "b", "c", "d", "e"}
    fmt.Println("Original Array:", originalArray)

    // Create a slice that "views" elements at index 1 through 3.
    // It looks at "b", "c", "d".
    viewSlice := originalArray[1:4]
    fmt.Println("Slice (view):", viewSlice)

    // Now, change an element IN THE SLICE.
    viewSlice[0] = "X" // This changes the element at index 0 of the slice ("b").

    // Look at the original array again. It changed too!
    fmt.Println("Slice after change:", viewSlice)
    fmt.Println("Original Array after change:", originalArray)
}

Output:

Original Array: [a b c d e]
Slice (view): [b c d]
Slice after change: [X c d]
Original Array after change: [a X c d e]

This happens because both originalArray and viewSlice were looking at the same block of memory. This is a powerful but tricky feature.

Here is a diagram showing the append process:

graph LR
  subgraph slice_append_logic["Slice Append Logic"]
    A[Start with append slice item] --> B{Enough capacity?}
    B -->|Yes| C[Add item to existing array]
    C --> D[Update slice length]
    D --> F[End]
    B -->|No| E[Allocate new bigger array]
    E --> G[Copy old items]
    G --> H[Add new item]
    H --> I[Point slice to new array]
    I --> F
  end

An ASCII diagram describing the append logic: Check if slice capacity is sufficient. If yes, add the item and update length. If no, allocate a new bigger array, copy old items, add the new one, and point the slice to this new memory location.


Common Pitfalls

  • Forgetting to re-assign append: Writing append(mySlice, "new") without mySlice = is a common bug. The new item is added, but you throw away the new, updated slice.
  • Mixing up len and cap: Using cap() in a for loop instead of len() can cause your program to crash by trying to access elements that exist in memory but aren't officially part of the slice's length yet.
  • Unexpected modifications: If you pass a slice to a function and that function changes an element, the original slice outside the function will also be changed because they both point to the same underlying array data.

FAQ

  1. So, when should I ever use an array?

When the size is truly fixed and known before the program runs. For example, a color value might be var red [3]uint8 for R, G, and B values. It will never need a fourth value. This is rare.

  1. What's the difference in syntax again?

[5]int has a number inside the brackets. This is an array of size 5. []int has empty brackets. This is a slice.

  1. Is a slice just a pointer?

Not quite, but close. A slice is a small struct that holds three things: a pointer to the start of the data in the array, its length, and its capacity. It's a lightweight wrapper.

  1. Why did the capacity jump from 3 to 6?

Go tries to be smart. When it resizes, it often doubles the capacity to avoid having to resize again too soon. This makes append operations more efficient on average.


Recap

  • Arrays are rigid and fixed in size. Think egg cartons.
  • Slices are flexible "views" of an array's data. Think shopping lists.
  • The append function is how you grow a slice. Always remember to assign its return value: mySlice = append(mySlice, newItem).
  • As a beginner, default to using slices. You'll solve 99% of your problems with them. Happy building!