Alright, let’s talk about the three-index slice. You’ve probably been happily slicing away with a[low:high] and thinking that’s all there is to it. But Go, in its infinite wisdom (or perhaps its obsession with giving you just enough rope to hang yourself with, elegantly), offers a third index. It looks like this: a[low:high:max].

This isn’t just for show. It’s the scalpel to the [low:high] machete. It gives you precise, surgical control over the resulting slice’s capacity.

Remember, a slice is a triplet: a pointer to an underlying array, a length, and a capacity. The standard two-index slice operation sets the new slice’s capacity to be from the low index to the end of the original underlying array. The three-index form lets you dictate where that capacity ends by specifying the max index.

Why You’d Bother: The append() Dilemma

The primary reason for this feature is to avoid unintentionally mutating other slices. Let’s look at the classic foot-gun that append provides when sharing an array.

package main

import "fmt"

func main() {
    // Let's make a slice with some data
    data := []string{"a", "b", "c", "d", "e"}
    // Now, let's take a slice of the first two elements.
    firstTwo := data[0:2] // length=2, capacity=5
    fmt.Printf("firstTwo: len=%d, cap=%d, %v\n", len(firstTwo), cap(firstTwo), firstTwo)
    // firstTwo: len=2, cap=5, [a b]

    // Now, let's append to firstTwo. There's plenty of capacity!
    firstTwoAppended := append(firstTwo, "ZOMG")
    fmt.Printf("firstTwoAppended: %v\n", firstTwoAppended)
    // firstTwoAppended: [a b ZOMG]

    // Let's check the original data slice. Oh no.
    fmt.Printf("data: %v\n", data)
    // data: [a b ZOMG d e]
}

See that? We appended to firstTwo and it overwrote the third element ("c") in the original data array, which firstTwo was still sharing. This is often not what you want. It’s a side effect that can cause bugs which are spectacularly annoying to track down. The designers gave us a incredibly powerful tool, but they also gave us this specific way to shoot ourselves in the foot with it. The three-index slice is the safety catch.

How the Third Index Saves the Day

The max parameter in a[low:high:max] sets the capacity of the new slice to max - low. This effectively creates a new slice that is “bounded” and cannot see or affect any elements in the original array beyond index max-1.

Let’s fix the previous example.

package main

import "fmt"

func main() {
    data := []string{"a", "b", "c", "d", "e"}

    // Use three-index slicing. We want a slice from index 0 to 2 (exclusive),
    // and we set the capacity to also end at index 2 (exclusive).
    // So: capacity = 2 - 0 = 2.
    firstTwo := data[0:2:2] // length=2, capacity=2
    fmt.Printf("firstTwo: len=%d, cap=%d, %v\n", len(firstTwo), cap(firstTwo), firstTwo)
    // firstTwo: len=2, cap=2, [a b]

    // Now, let's append to firstTwo.
    firstTwoAppended := append(firstTwo, "ZOMG")
    fmt.Printf("firstTwoAppended: len=%d, cap=%d, %v\n", len(firstTwoAppended), cap(firstTwoAppended), firstTwoAppended)
    // firstTwoAppended: len=3, cap=4, [a b ZOMG] 
    // ^^ Notice the capacity is now 4? It allocated a new array!

    // Let's check the original data. It's untouched. Victory.
    fmt.Printf("data: %v\n", data)
    // data: [a b c d e]
}

By setting the capacity to exactly the length (2:2), the next call to append has no room to grow within the original array. It is forced to allocate a new underlying array for firstTwoAppended. The original data slice and its array remain completely unaffected. This is the intended use case: creating a slice that is effectively detached from the rest of the original array for future appends.

The Rules and The Edge Cases

The indices must obey the following rules, or the compiler will panic at you. It’s not a runtime error; it’s a compile-time one. Go is being strict here because getting this wrong is a fundamental logic error.

0 <= low <= high <= max <= cap(a)

Let’s break that down:

  • low <= high: Your length (high - low) can’t be negative.
  • high <= max: Your new slice’s length can’t exceed its new capacity (max - low). This makes sense—you can’t have a window that’s wider than the wall it’s on.
  • max <= cap(a): You can’t pretend the original array is bigger than it actually is.

Trying to break these rules is a compile-time error:

func main() {
    a := []int{1, 2, 3, 4, 5}
    // good := a[1:3:4] // OK: low=1, high=3, max=4 -> 1<=3<=4<=5
    bad := a[1:3:6] // Compiler Error: invalid slice indices: 6 > 5 (cap(a))
}

Best Practices and When to Wield This Tool

Use the three-index slice when you are creating a sub-slice that you intend to append to, and you want to be absolutely, 100% certain that you will not clobber the original slice’s data. It’s a declaration of intent: “This slice is its own thing now; leave the original array alone.”

It’s especially crucial when returning a slice from a function. If your function internally uses a large allocated slice and returns a small part of it, using a three-index slice to limit the capacity ensures the caller’s append operations don’t accidentally corrupt your function’s internal state (or vice-versa).

It’s not something you’ll use on every line of code, but when you need it, it’s indispensable. It turns a potential source of subtle, horrifying bugs into a clean, predictable operation. It’s Go’s way of saying, “I trust you to manage memory, and here’s the precise control to do it right.”