Video Coming Soon...

Created by Zed A. Shaw Updated 2025-10-07 14:32:54

17: The Type System: Arrays and Slices

WARNING This exercise is in DRAFT status, so there may be errors. If you find any, please email me at help@learncodethehardway.com so I can fix them.

An array is any sequence of data. Another name for array is a "list", as in you create a list of integers from 1 to 10. Or a list of groceries to get from the store. The word array typically means "fixed size list" in many languages, and Go adopts this terminology too.

Go also has the concept of a slice, which is a more flexible version of an array. With a slice you can expand it, shrink it, and you can get slices from the middle part of an array, thus why it's called a "slice."

While the concept of an array is simple, how they're used in Go (and most languages) can be very complex. This exercise will be quite large because of this complexity, but by the end you should know enough about array and slice to do almost anything you need.

How to Approach this Exercise

This exercise is different because I have to cover so much, and doing a full code example of every concept would require way too much space. Instead of me making a full main.go for each concept I want you to make one and try out what I show you here. You should start doing this from now on because you really only understand concepts in programming after you use them in real code.

Creating an array

Let's start with array first. You create an array like other types, but you use the [] syntax like this:

var cheeses [4]string

Let's break down each part of this so you know what it's doing:

Here's a few more examples for you to study:

var pets [3]Animals
var counts [100]int
var stats [4]float64

Filling an array

You can fill in the contents of an array by using the [] to index into it like this:

cheeses[0] = "Cheddar"
cheeses[1] = "Parmesan"
cheeses[2] = "Swiss"
cheeses[3] = "Velveeta" // lol, "cheese"

One concept to understand is that the [] operators have different meaning in different uses. In the var declaration [4] means "of size 4" but in the cheeses[1] usage it means "get the string at 1". You'll have to remember both uses.

About Zero Indexing

You may have noticed I started with 0 (zero) when setting my array of cheeses. This isn't a mistake, but rather the way most languages start indexing because you are doing random access, not sequenced access. If we talk about horses you'll understand.

Imagine you're at a horse race and you've bet on Dapper Gentleman to win and he does. When tell your friends what happened what do you say? Do you say, "Dapper Gentleman came in zeroth." No, you say "Dapper Gentleman came in first." You say this because the order of the horses is explained randomly because there's an element of time preventing you from simply changing the order. The same applies to dates and time. There's no "zero month" because you can't simply time travel to a random month. They go in order, first January, then February, and so on forever, so there is no "zeroth" month, only a "first month."

An array in Go is different because you can access any element you want at any time. If you want to access the element at index 2 you aren't forced to get element 0 and 1 first. You can just get 2 and that's it.

Zero is used because of the random nature if index, but also because a lot of important math just works better when indexing starts with 0. It's a little too complex to get into, but a lot of smart people figured this out decades ago so that's what we use.

Converting to Zero Indexing

Based on this the best way to convert indexing in your head is to understand that you think in terms of time, but the computer wants random access. You will say, "I want the 3rd element." The computer wants the element at index 2.

That means, any time you write "nd", "rd", or "th" subtract one. If you say "I need the 2nd" that's "nd" so subtract 1 and you get "index 1." If you say "get the 10th" then that's "th" so subtract 1.

You should also use this wording when doing this: "I want the 10th element so that's the one at index 9." This use of "at index X" will help you understand the difference better over time.

Slicing Parts of an array

The next concept is slicing out sections of an array. In go this is called a slice...because that's what it is. You can make a slice with the [:] syntax like this:

best_cheese := cheeses[1:3]

This tells Go to create a slice that contains the elements at 1 and 2 of the array cheeses. This new slice will then have strings with "Parmesan" and "Swiss", because they are the best.

Why is "Velveeta" not included when I write [1:3]? Because "Velveeta" is disgusting and not even cheese. It's like some oil with some fake flavor in it and just enough cheese like substance that they can call it cheese. Oh! Do you mean why didn't Go also include the element at index 3 in cheese? Sorry, I just really hate "Velveeta."

The reason "Velveeta" isn't included is because Go's slice syntax is "not inclusive." That means it returns "X through Y, not including Y." Another way to word that is, "values from X up to but not including Y."

Slices Reference Arrays

When you create a slice with the [:] syntax you are not creating a new array. You are instead creating a reference to those contents of the original array. That means if you assign values to the slice it'll change the source array like this:

best_cheese := cheeses[1:3]
best_cheese[0] = "Baby Swiss"

fmt.Println(cheeses)

In this code I'm setting the 0 element of best_cheese to "Baby Swiss" but that also changes the cheese[1] element. If you run this it'll print out that change.

Try it and play with this until you get it, but keep in mind that you need to learn about Pointers later to totally understand what's going on. Just remember that when you change a slice you change the array.

Using make() and append()

There are many times when you don't know the size of an array, and the make() built-in function solves that. You can use make() to create an array of any size, have an initial capacity, and it returns a slice so you can dynamically resize it.

Let's say you want to create an array to store any number of cheeses (just not Velveeta):

all_cheese := make([]string, 0)

This creates an empty array, then gives you a slice to work with so you can modify it. This slice is isn't very useful because if you try to add anything you get an error:

$ ./ex17
panic: runtime error: index out of range [0] with length 0

To add to all_cheese we need to use the append() function like this:

all_cheese := make([]string, 0)
all_cheese = append(all_cheese, "Muenster")

Doing this will add the string "Muenster" to all_cheese, but why is append() returning a new variable?

Remember how I said that make() returns a slice pointing at an array? Well ask yourself, what happens if the underlying array can't hold any more elements? In that case append() would need to make a new array that has the capacity to hold more, and return a new slice pointing at that new array.

NOTE I need to confirm this is true.

That means append() is doing something like this:

  1. Is there enough space for the new element?
  2. If yes, then it at the end and return the same slice.
  3. If not, then append creates a new array, copies everything over, and returns a slice for the new array.
  4. When making the new array, if the current capacity is <= 1024 it doubles the new array capacity. If it's over 1024 then it only adds 25% more capacity.

That's why it needs your original slice and returns a new one. Otherwise you couldn't have append() reliably expand an array.

append() and the ... Operator

The append() function takes more than one parameter, which lets you add multiple things to the end of a slice:

numbers := make([]int, 4)
numbers = append(numbers, 1, 2, 3, 4)

What happens if you want to add the contents of one slice (array) to the end of another? That's where the ... (spread) operator comes in. The ... (spread) operator takes an array and converts its contents to a list of parameters to a function. You can use it with any function, but with append() it looks like this:

numbers := make([]int, 0, 4)
numbers = append(numbers, 1, 2, 3, 4)

more_numbers := make([]int, 0, 3)
more_numbers = append(more_numbers, 5, 6, 7)

// now to concatenate (concat) the two
numbers = append(numbers, more_numbers...)

There's many functions that expect you to use ... for basic operations, so play with it some more.

How Capacity Works

There's a third parameter to make() that specifies an initial capacity:

all_cheese := make([]string, 0, 10)

This tells make() to create an array with 0 contents, but give an initial capacity of 10. This makes it so append() doesn't have to immediately make a new array the first time you call it.

You can find out the capacity of an array with the cap() function like this:

all_cheese := make([]string, 0, 10)
fmt.Println("Capacity is", cap(all_cheese))

This should print out Capacity is 10.

An Important slice Footgun

Go uses a technology called Garbage Collection to manage the memory your code uses. There's many ways to write a GC (Garbage Collector) but usually it involves keeping track of what's been allocated, and when no part of your program uses something the GC deletes it.

When you make a small slice of a giant array you can run into an issue where the GC keeps that giant array around even if you don't need it anymore. Imagine this code:

big_array := make([]int, 0, 100000)
// fill big_array with stuff
small_slice := big_array[1:3]

Even though small_slice is 2 elements in size it will keep the big_array in memory long after its necessary. To fix this you simply make a copy of slices from big arrays:

big_array := make([]int, 0, 100000)
// fill big_array with stuff
small_slice := make([]int, 0, 2)
copy(small_slice, big_array[1:3])

This creates a new array named small_slice and copies the requested big_array[1:3] into it. That means when big_array isn't used anymore the GC can collect it.

WARNING You will most likely not run into this problem until you get more serious about Go, but I'm going to STRONGLY warn you that obsessing over this kind of thing will do nothing but make your code a brittle, difficult to read, weird, little box of turds. It's always better to write your code in the simplest most natural way and then find places that need to be improved like this after it's working. Working code can be made faster, but broken code is just broken no matter how fast it is.

Built-In Functions for slice

The full list of built-in functions is available online. These are "built into Go" so you don't import this package. These are the functions that help with array:

The slices Package

The slices package in the Go standard library contains many useful functions for working with slice and array data. Before you try leveraging append() to do something, see if the slices package has what you need. In the past Go programmers would hand roll most of these operations but today it's not necessary (and doing that probably produces a bunch of wonderful bugs).

The Code

I'm not going to do a full breakdown of this code like normal because you should have been doing little experiments of each concept as we discussed them. You did that right? You didn't just skim what I wrote and avoided typing in examples of each thing? Yes? Then great, here's some code for you to study and you should know what it does.

View Source file ex17/main.go Only

package main

import (
    "fmt"
)

type Animal struct {
    Name string
    Age int
    Species string
    Weight float64
}

func main() {
    var vet [4]Animal

    vet[0] = Animal{"Doug", 5, "Dog", 25.0}
    vet[1] = Animal{"Catherine", 1, "Cat", 2}
    vet[2] = Animal{"Gary", 1, "Goldfish", 0.1}
    vet[3] = Animal{"Percy", 10, "Python", 50.0}

    for i, animal := range vet {
        fmt.Println("animal at", i, "is", animal)
    }

    // get a slice of the middle 2, X:Y is not inclusive 
    middle := vet[1:3]
    fmt.Println("middle has", len(middle), "elements")

    // if you don't need i replace with _
    for _, animal := range middle {
        fmt.Println("middle is", animal)
    }

    // adds one more to this slice !!footgun
    middle = append(middle, Animal{"Jack", 1, "Jaybird", 0.1})

    for i, animal := range middle {
        fmt.Println("middle", i, "is", animal)
    }

    // changing that slice changes the original vet !!footgun
    middle[0].Name = "CHANGED"
    fmt.Println("the one I changed", middle[0])
    fmt.Println("original is", vet[1])

    // copy is good for small slices of big arrays
    // why: small slices still reference the big array 
    // so the GC retains: !!footgun memory leaking on slices
    front := make([]Animal, 2)
    copy(front, vet[0:2])
    front[0].Name = "WONT CHANGE"
    fmt.Println("front one", front[0])
    fmt.Println("original unchanged", vet[0])

    // using ... to copy
    numbers := make([]int, 0, 4)
    numbers = append(numbers, 1, 2, 3, 4)

    var new_numbers []int
    new_numbers = append(new_numbers, numbers...)

    new_numbers[0] = 100
    fmt.Println("numbers:", numbers)
    fmt.Println("new_numbers:", new_numbers)

    // concat with append
    numbers = append(numbers, new_numbers...)
    fmt.Println("concat numbers:", numbers)
}

The Practice

  1. Break It -- Give the vet array a size 0 to see what happens when you try to set an array element that isn't available.
  2. Change It -- Really spend some time changing this code by finding ways to use the functions in the slice package now.
  3. Recreate It -- As usual, use your ever growing powers of Code Recall to recreate this code. An alternative is to just make a new piece of code that does something totally different, but try not to look at the documentation while you do it.

Study Drills

  1. Go through the slices package and try out all of the examples.
  2. If you know how to append() the contents of one slice to the end of another, then can you figure out how to use append() to copy? Hint: You need ....
  3. Using what you know about reading input from the last module, can you store a user's input into a slice?
Previous Lesson Next Lesson

Register for Learn Go the Hard Way

Register today for the course and get the all currently available videos and lessons, plus all future modules for no extra charge.