Skip to Content
Course content

180: Common Go Interview Questions on Slices vs Arrays

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Why would I ever use an array if slices are so much more flexible?

I get this question a lot. On the surface, arrays seem like a restricted version of slices. You're right that in 95% of your Go code, you'll use slices. But arrays have a specific property: their size is part of their type. An [3]int is a completely different type than a [4]int.

You use arrays when you have a fixed mathematical or structural constraint that should never change. Think of an RGB color value or a 3D coordinate. If you're representing a point in 3D space, you don't want a slice that could accidentally grow to 4 dimensions.

type Point3D [3]float64

func calculateDistance(p Point3D) float64 {
    // The compiler guarantees p always has exactly 3 elements.
    // No need to check len(p) before accessing p[2].
    return p[0] * p[0] + p[1] * p[1] + p[2] * p[2] 
}

Another huge difference is that arrays are value types. If you assign one array to another, Go copies the entire thing. Slices, however, are just headers pointing to an underlying array. Copying a slice header is cheap; copying a 10,000-element array is not.

If I pass a slice to a function and change an element, does the caller see it?

Yes, they do. And this is a classic interview "gotcha." To understand why, you have to stop thinking of a slice as "a list" and start thinking of it as a slice header. That header contains a pointer to the underlying array, a length, and a capacity.

When you pass a slice to a function, Go passes that header by value. You get a copy of the pointer, the length, and the capacity. Since the copy of the pointer still points to the same memory address as the original, modifying an element affects the same underlying data.

func updateFirstElement(data []int) {
    if len(data) > 0 {
        data[0] = 999 // This modifies the underlying array
    }
}

func main() {
    scores := []int{10, 20, 30}
    updateFirstElement(scores)
    fmt.Println(scores[0]) // Prints 999
}

What's actually happening during an append?

This is where things get tricky. append doesn't just "add an item." It checks if the underlying array has enough capacity to hold the new element. If it does, it just updates the element and returns a new header with an incremented length.

But if the capacity is full, Go does something expensive: it allocates a brand new, larger array, copies all the old elements over, adds the new one, and returns a header pointing to this new location. If you're passing a slice into a function and calling append inside that function, the caller might not see the new elements because the function's local slice header was updated to point to a new array, but the caller's header still points to the old one.

func tryToAdd(s []int) {
    s = append(s, 4) // If this triggers a reallocation, the caller's slice is unchanged
}

func main() {
    // Create a slice with length 3 and capacity 3
    nums := make([]int, 3, 3) 
    tryToAdd(nums)
    fmt.Println(len(nums)) // Still 3! The append happened on a copy of the header.
}

This is why you almost always see append used as s = append(s, val). You must capture the returned header to ensure you're tracking the correct underlying array.




📋 Practical Task

Fixing the Slice-Header Mutation Bug

You are reviewing a teammate's code for a system that processes a batch of sensor readings. The goal is to have a function called filterOutliers that removes any reading above 100. However, the teammate complained that the original slice in main isn't actually being updated when the function runs.

Your Task: Modify the filterOutliers function or the way it is called in main so that the original slice is correctly updated to reflect the filtered results. You can choose to return the new slice or use a pointer to the slice header.

package main

import "fmt"

func filterOutliers(readings []float64) {
    var filtered []float64
    for _, v := range readings {
        if v <= 100.0 {
            filtered = append(filtered, v)
        }
    }
    // BUG: The local 'filtered' slice is updated, 
    // but the 'readings' slice in main remains unchanged.
    readings = filtered 
}

func main() {
    data := []float64{23.5, 105.2, 44.1, 110.0, 12.2}
    fmt.Printf("Before: %v\n", data)
    filterOutliers(data)
    fmt.Printf("After: %v\n", data) // Currently prints the original unfiltered list
}
Rating
0 0

There are no comments for now.

to be the first to leave a comment.