The crash that stops everything
You are processing a batch of records. The loop runs smoothly for a hundred items. Then it hits the last one. The code tries to peek at the next element to check for a delimiter. The runtime screams and kills the program. The stack trace points to a simple line: next := data[i+1]. The message is blunt: panic: runtime error: index out of range [100] with length 100. The program is dead. The user gets a broken page.
This happens when code assumes data exists at a position where it does not. Go arrays and slices enforce strict bounds. Accessing an index that falls outside the valid range triggers a panic. The panic is a safety mechanism. It stops the program before it reads garbage memory or corrupts data. Fixing this requires understanding how indices relate to length and adding checks before access.
How indices and length work
Go sequences like arrays and slices store elements contiguously. Every element has a numeric index starting at zero. If a slice has five elements, the valid indices are 0, 1, 2, 3, and 4. The length is 5. Index 5 is the position immediately after the last element. It does not contain data.
The length represents the count of elements. The highest valid index is always length - 1. Accessing index length or higher asks the runtime to read past the allocated block. Accessing a negative index asks the runtime to read before the start. Both are invalid. Go performs a bounds check at runtime for every array or slice access. The check compares the index against the length. If the index is less than zero or greater than or equal to the length, the runtime panics.
len is a built-in function. It works on arrays, slices, maps, strings, and channels. It returns the count of elements. The call is fast. It does not allocate memory. You can call len repeatedly in a loop without performance penalty.
Convention aside: Go code runs through gofmt. The formatting is standard. Focus on the logic, not the braces. Most editors run gofmt on save. Trust the tool to handle indentation and spacing.
Minimal example
This example shows a direct access that exceeds the slice length.
package main
import "fmt"
// AccessSlice demonstrates the bounds check failure.
func AccessSlice() {
// Slice has three elements. Indices 0, 1, 2 are valid.
items := []string{"alpha", "beta", "gamma"}
// Index 3 is equal to len(items).
// This is the first invalid index.
// The runtime will panic here.
fmt.Println(items[3])
}
func main() {
AccessSlice()
}
Running this program produces a panic. The output includes the panic message and a stack trace. The message tells you the index and the length. The stack trace shows the function and line number. Use this information to locate the bug.
Indices are zero-based. Length is one-based. The gap between them is where panics live.
What the runtime does
When the program executes, the Go runtime inserts a bounds check before the memory access. The check is a simple integer comparison. It compares the index value against the slice length. If the check fails, the runtime calls panic. The panic unwinds the stack. It prints the trace. It terminates the goroutine. If no recover call catches the panic, the process exits with a non-zero status.
The panic message format is consistent. It reads panic: runtime error: index out of range [N] with length M. N is the index you tried to access. M is the length of the slice or array. If N is negative, the message shows [-1]. If N is larger than M, the message shows the actual values. The numbers are the key. They tell you exactly what went wrong.
The runtime does not skip bounds checks for performance. The safety guarantee is part of the language contract. Go prioritizes correctness over speed in this area. The check is cheap. The cost of reading invalid memory is high.
Read the panic message. The index and length are right there. Trust the numbers.
Realistic scenario: processing pairs
Real code often processes data in chunks. A common pattern is iterating over a slice in steps. This requires careful bounds checking. The loop variable might advance past the last element. Accessing the next element without checking causes a panic.
package main
import "fmt"
// ProcessPairs iterates over a slice in steps of two.
// It handles the case where the slice has an odd number of elements.
func ProcessPairs(pairs []int) []int {
var results []int
// Step by 2 to process pairs.
for i := 0; i < len(pairs); i += 2 {
// Check if a second element exists for the current pair.
// i+1 might equal len(pairs) on the last iteration.
if i+1 < len(pairs) {
// Sum the pair.
results = append(results, pairs[i]+pairs[i+1])
} else {
// Odd element left over. Handle it separately.
results = append(results, pairs[i])
}
}
return results
}
func main() {
data := []int{10, 20, 30, 40, 50}
fmt.Println(ProcessPairs(data))
}
The loop increments i by 2. When i is 4, the loop body runs. i+1 is 5. The length is 5. The check i+1 < len(pairs) fails. The code handles the odd element. Without the check, pairs[i+1] would panic. The check ensures the second index is valid before access.
Check the length before you reach for the index. The length tells you the truth.
Sub-slicing and the high bound
Sub-slicing creates a new slice from a portion of an existing slice. The syntax is s[low:high]. The runtime checks both low and high against the length. low must be less than or equal to high. high must be less than or equal to the length. low must be non-negative. Violating any of these rules causes a panic.
package main
import "fmt"
// SubSlice demonstrates bounds checking on slice expressions.
func SubSlice(data []byte) []byte {
// If data has length 4, indices 0..3 are valid.
// Attempting to slice up to index 5 panics.
// The runtime checks both low and high against length.
return data[0:5]
}
func main() {
buffer := []byte{1, 2, 3, 4}
fmt.Println(SubSlice(buffer))
}
This code panics with panic: runtime error: slice bounds out of range [0:5] with length 4. The high bound 5 exceeds the length 4. The fix is to cap the high bound at the length.
// SafeSubSlice returns a sub-slice capped at the length.
func SafeSubSlice(data []byte, end int) []byte {
// Cap end at len(data) to prevent out of range panic.
if end > len(data) {
end = len(data)
}
return data[0:end]
}
Sub-slicing is a frequent source of panics. Always verify the bounds before slicing. The high bound can equal the length. The low bound cannot.
Sub-slicing checks both ends. Cap the bounds before you slice.
Maps do not panic
The kernel of this topic sometimes mentions maps. Maps behave differently. Accessing a map with a missing key returns the zero value for the value type. It does not panic. If the value type is a string, you get an empty string. If it is an int, you get zero. If it is a pointer, you get nil.
package main
import "fmt"
// MapAccess shows that maps return zero values for missing keys.
func MapAccess() {
m := map[string]int{"a": 1, "b": 2}
// Key "c" does not exist.
// This does not panic.
// val is 0, the zero value for int.
val := m["c"]
fmt.Println(val)
}
func main() {
MapAccess()
}
If you need to know whether a key exists, use the two-value form. The second value is a boolean. It is true if the key exists, false otherwise.
// CheckKey demonstrates the two-value map access.
func CheckKey() {
m := map[string]int{"a": 1}
// ok is true if "a" exists, false if "z" does not.
val, ok := m["z"]
if !ok {
fmt.Println("key not found")
}
}
Maps are for lookups by key. Slices are for ordered sequences by index. Use the data structure that matches the access pattern. Maps return zero. Slices panic. Pick the right tool.
Common pitfalls and error messages
Several patterns lead to index out of range panics. Recognizing these patterns helps prevent bugs.
The loop condition i <= len(s) is a classic error. The loop runs one iteration too many. The last iteration accesses s[len(s)], which is invalid. Use i < len(s) instead.
Accessing s[i+1] inside a loop without checking i+1 < len(s) is another common mistake. The loop might be valid for i, but i+1 can exceed the length. Check the next index before access.
Empty slices cause panics if code assumes at least one element. Accessing s[0] on an empty slice panics. Check len(s) > 0 before accessing the first element.
Negative indices panic. Go does not support negative indexing like Python. Accessing s[-1] panics. Validate input indices to ensure they are non-negative.
The panic message always includes the index and length. Use this information. If the message says index out of range [5] with length 5, the code tried to access index 5 on a slice of length 5. The fix is to ensure the index is less than 5.
The worst goroutine bug is the one that never logs. Panics log automatically. Read the log. Find the index and length. Fix the check.
Converting panics to errors
Panics are for unrecoverable errors. A bounds check failure is usually a bug. If the input comes from external data, a bounds check failure indicates invalid input. In this case, return an error instead of panicking. Wrap the access in a function that checks bounds and returns an error.
package main
import "fmt"
// SafeGet returns an element or an error if the index is invalid.
// This converts a potential panic into a recoverable error.
func SafeGet(slice []string, index int) (string, error) {
// Check bounds explicitly.
if index < 0 || index >= len(slice) {
return "", fmt.Errorf("index %d out of range", index)
}
return slice[index], nil
}
func main() {
items := []string{"x", "y"}
val, err := SafeGet(items, 5)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Value:", val)
}
This approach handles invalid input gracefully. The caller receives an error. The program continues. Use this pattern for public APIs or functions that process untrusted data. Internal code can rely on panics to catch bugs during development.
if err != nil is verbose by design. The community accepts the boilerplate because it makes the unhappy path visible. Check errors explicitly. Do not ignore them.
Don't fight the type system. Wrap the value or change the design.
Decision matrix
Use a manual bounds check when you need to access an element by a calculated index that might exceed the slice length.
Use a range loop when you need to iterate over every element or every key-value pair without managing indices manually.
Use a map when you need to look up values by an arbitrary key and want the zero value returned for missing keys instead of a panic.
Use a slice sub-slice operation when you need to extract a contiguous portion of data, as the runtime checks bounds for the sub-slice expression.
Use the two-value map access when you need to distinguish between a missing key and a key that holds the zero value.
Use plain sequential code when you don't need concurrency: the simplest thing that works is usually the right thing.
Range is your friend. Indices are for math. Maps are for lookups. Pick the tool that matches the access pattern.