Fix

"slice bounds out of range" in Go

Fix Go slice bounds out of range panic by checking slice length before accessing indices.

When the index exceeds the data

You write a function to extract the third column from a CSV line. The first hundred rows process without issue. The hundred and first row is malformed and only contains two fields. Your program crashes. The terminal fills with a stack trace pointing to a single line of code. The runtime stops everything because you asked for an index that does not exist. This is the slice bounds out of range panic. It is Go's way of telling you that you made an assumption about data that turned out to be false.

How slices actually work

Go slices are not arrays. They are lightweight descriptors that point to an underlying array. Every slice carries three pieces of information: a pointer to the first element, a length, and a capacity. The length tells the runtime how many elements are currently valid. The capacity tells you how many elements the backing array can hold before it needs to grow. When you write mySlice[2], the runtime checks whether 2 is strictly less than the length. If it is not, the program panics.

Think of a slice like a numbered ticket queue at a concert. The length is the number of tickets handed out so far. The capacity is the total number of seats in the venue. If you try to hand out a ticket number that exceeds the count of tickets already printed, the system rejects it. Go does not return nil, undefined, or an empty string. It stops execution immediately. This design choice prevents silent data corruption and forces you to handle missing data explicitly.

Slices are cheap to copy because they only copy the header, not the underlying array. Passing a slice to a function does not duplicate the data. It hands over a new view of the same memory. This is why you can safely pass large datasets through call stacks without performance penalties. The trade-off is that you must respect the boundaries of that view.

Slice headers are transparent to the programmer, but knowing they exist explains why length and capacity behave differently. You can read both with len() and cap(). You cannot modify them directly. You change them by appending, slicing, or creating a new slice. Trust the bounds. Verify them before indexing.

The minimal safe pattern

package main

import "fmt"

// GetFirstElement returns the first item in a slice, or a zero value if empty.
func GetFirstElement(items []string) string {
	// Check length before indexing to avoid a runtime panic.
	if len(items) == 0 {
		return "no items"
	}
	// Safe to access index 0 because the length check guarantees at least one element.
	return items[0]
}

func main() {
	// Demonstrate behavior with populated and empty slices.
	fmt.Println(GetFirstElement([]string{"apple", "banana"}))
	fmt.Println(GetFirstElement([]string{}))
}

The pattern is simple. Verify the boundary. Act on the result. Go does not hide missing data behind optional types or silent failures. You write the check, you own the outcome.

When you slice a slice, the new slice inherits the capacity of the original but gets its own length. mySlice[1:3] creates a view starting at index 1 and ending before index 3. The length becomes 2. The capacity becomes the original capacity minus 1. If you index the new slice, the runtime still checks against its own length field. The bounds check is always relative to the slice you are accessing, not the backing array.

Always check len() before indexing. Never assume a slice contains data.

What happens at runtime

When the compiler translates your code, it emits a bounds check instruction for every slice access. At runtime, the Go scheduler executes that instruction before dereferencing the memory address. If the index falls outside the valid range, the runtime calls panic with a descriptive message. The panic unwinds the current goroutine's stack, running any deferred functions along the way. If you do not recover from the panic, the program terminates.

This behavior differs from languages that return null or throw a catchable exception by default. Go treats out-of-bounds access as a programming error that should fail fast. The philosophy is that a crashed program is easier to debug than a program that silently processes garbage data. You can catch panics with recover, but that is reserved for truly exceptional cases, not routine control flow.

The bounds check is extremely fast. Modern CPUs predict branch outcomes, and the Go compiler places the check right next to the memory load. The performance cost is negligible compared to the safety guarantee. You do not need to optimize away length checks. The compiler and runtime are designed to handle them efficiently.

Panic recovery is a blunt instrument. Use it at the top of a goroutine or in a server framework to prevent process termination. Do not use it to replace error returns. The community convention is to return errors for expected failures and panic only for unrecoverable state corruption.

Fail fast. Log the state. Fix the assumption.

Real world data handling

Real code rarely deals with perfectly shaped data. Consider an HTTP handler that reads a list of user IDs from a query parameter. The parameter arrives as a comma-separated string. You split it into a slice and try to grab the second ID for a secondary action.

package main

import (
	"fmt"
	"strings"
)

// ProcessUserIDs extracts and prints the primary and secondary user IDs.
func ProcessUserIDs(rawIDs string) {
	// Split the raw string into a slice of individual IDs.
	ids := strings.Split(rawIDs, ",")

	// Always verify the slice has enough elements before accessing higher indices.
	if len(ids) < 2 {
		fmt.Println("Missing secondary ID. Only primary ID will be processed.")
		if len(ids) > 0 {
			fmt.Println("Primary:", ids[0])
		}
		return
	}

	// Both indices are safe because the length check guarantees at least two elements.
	fmt.Println("Primary:", ids[0])
	fmt.Println("Secondary:", ids[1])
}

func main() {
	// Test with complete, partial, and empty input.
	ProcessUserIDs("alice,bob,charlie")
	ProcessUserIDs("dave")
	ProcessUserIDs("")
}

The function handles three states: complete data, partial data, and empty input. Each state gets explicit handling. The code reads like a decision tree rather than a gamble. You never reach for an index without confirming the slice can support it.

Go 1.21 introduced the slices package in the standard library. It provides safe, idiomatic ways to search, find, and transform slices without manual index management. slices.Contains checks for existence. slices.Index returns the first matching index or -1. slices.Clone creates a fully independent copy. These functions eliminate boilerplate and reduce the surface area for off-by-one mistakes.

The community convention is to prefer for _, v := range slice over index-based loops when you do not need the index. The range loop handles bounds internally and iterates exactly len(slice) times. It is safer and more readable.

Range over the data. Skip the index unless you need it.

Common traps and runtime messages

The most common trigger is an off-by-one error in a loop. Writing for i := 0; i <= len(slice); i++ includes the length as a valid index. Since slice indices start at zero, the highest valid index is len(slice) - 1. The compiler will not catch this. The runtime will stop you with panic: runtime error: slice bounds out of range [5] with length 5.

Another trap is confusing length with capacity. You can append to a slice up to its capacity without allocating new memory. Appending beyond capacity triggers a reallocation and returns a new slice header. If you hold a reference to the old slice and try to index it after another function call has grown the underlying array, you might read stale data or panic. Always use the slice returned by append. The language requires it because the backing array might have moved.

Modifying a slice while ranging over it is equally dangerous. The for range loop captures the length at the start of the iteration. If you append elements inside the loop, the loop will not see them. If you delete elements or shrink the slice, you will eventually index past the new length. The runtime panic message will look like panic: runtime error: slice bounds out of range [:3] with length 2. The fix is to iterate over a copy, or iterate backwards when removing items.

JSON unmarshaling is another frequent source of bounds panics. When you decode into a []any or []string, the decoder creates a slice with exactly the number of elements in the JSON array. If your code assumes a fixed size, it will crash on malformed payloads. Always validate the decoded length before accessing specific positions.

Go developers accept this strictness because it eliminates an entire class of memory safety bugs. The trade-off is writing explicit checks. The community convention is to treat missing data as a normal condition, not an exception. You write the check, you return early, you move on.

Verify before you index. Panic is a last resort, not a control flow tool.

Choosing the right tool

Use a length check when you need to access a specific index that might not exist. Use a for range loop when you need to process every element without tracking indices manually. Use the slices package when you need safe searching, finding, or transforming on Go 1.21+. Use a map when you need O(1) lookups by key instead of linear index access. Use a zero value fallback when missing data is expected and acceptable.

Where to go next