Understanding the 'panic: runtime error: index out of range' Error in Go
The panic: runtime error: index out of range message is one of the most common runtime panics you'll encounter when writing Go programs. It occurs when your code attempts to access an element of a slice or array using an index that is less than zero or greater than or equal to the length of the collection. Because Go does not have exceptions in the traditional sense, this panic will crash your program unless it is recovered, making it critical to understand, prevent, and handle properly.
What Exactly Triggers This Panic?
In Go, slices and arrays are zero-indexed data structures with a fixed or dynamic length. Any index operation slice[i] or array[i] must satisfy the condition 0 <= i < len(collection). When this condition is violated, the runtime immediately triggers a panic. The same rule applies when slicing with range expressions like slice[low:high] — both bounds must fall within the valid range of the underlying array.
Here is the simplest example that reproduces the error:
package main
import "fmt"
func main() {
data := []int{10, 20, 30}
fmt.Println(data[5]) // panic: runtime error: index out of range [5] with length 3
}
When you run this code, Go prints a detailed panic message that includes the offending index value and the length of the collection, which is invaluable for debugging:
panic: runtime error: index out of range [5] with length 3
Common Scenarios That Cause Index Out of Range
Let's walk through the most frequent situations where this panic arises in real-world Go code:
1. Off-by-One Errors in Loop Conditions
Using <= instead of < in a loop condition is a classic mistake. Since slices are zero-indexed, the last valid index is len(slice) - 1.
// INCORRECT — will panic on the last iteration
func printElements(s []string) {
for i := 0; i <= len(s); i++ {
fmt.Println(s[i])
}
}
// CORRECT
func printElements(s []string) {
for i := 0; i < len(s); i++ {
fmt.Println(s[i])
}
}
2. Accessing an Empty Slice or Array
Even index 0 is out of range when the slice has zero elements. Always check the length before indexing, especially when dealing with input from external sources like API responses, file reads, or database queries.
func firstElement(s []int) int {
// This panics if s is empty
return s[0]
}
// SAFE version
func firstElement(s []int) (int, bool) {
if len(s) == 0 {
return 0, false
}
return s[0], true
}
3. Negative Index Values
Negative indices are never valid in Go. If you compute an index dynamically and the calculation yields a negative number, a panic is inevitable.
func getNthFromEnd(s []int, n int) int {
// If n > len(s), this produces a negative index
return s[len(s)-n] // panics when len(s)-n < 0
}
// SAFE version
func getNthFromEnd(s []int, n int) (int, error) {
if n <= 0 || n > len(s) {
return 0, fmt.Errorf("invalid n: %d for slice of length %d", n, len(s))
}
return s[len(s)-n], nil
}
4. Slice Bounds Out of Range in Sub-slicing
When creating a sub-slice with s[low:high], both low and high must be within 0 to len(s), and low <= high. The same applies to s[low:high:max] for capacity-limited slices.
func subSlice(s []byte, start, end int) []byte {
return s[start:end] // panics if bounds are invalid
}
// SAFE version
func subSlice(s []byte, start, end int) ([]byte, error) {
if start < 0 || end < 0 || start > len(s) || end > len(s) || start > end {
return nil, fmt.Errorf("invalid slice bounds: [%d:%d] with length %d", start, end, len(s))
}
return s[start:end], nil
}
5. Concurrent Modification of Slice Length
In concurrent programs, one goroutine might shrink a shared slice while another attempts to read from an index that no longer exists. This is a race condition that leads to an intermittent, hard-to-reproduce panic.
// DANGEROUS: shared slice accessed from multiple goroutines
var sharedData []int
func reader() {
for i := 0; i < len(sharedData); i++ {
fmt.Println(sharedData[i]) // may panic if writer truncates the slice
}
}
func writer() {
// Could modify sharedData concurrently
sharedData = sharedData[:0]
}
The fix involves synchronisation using mutexes, channels, or by avoiding shared mutable state altogether.
6. String Indexing on Multi-byte Characters
Indexing into a string yields bytes, not runes. When working with UTF-8 strings containing multi-byte characters, indexing directly can slice in the middle of a character, potentially causing downstream panics when combined with length assumptions.
func firstRune(s string) rune {
// Incorrect for multi-byte characters — indexes bytes, not runes
// Also panics on empty string
return rune(s[0])
}
// SAFER approach using the unicode/utf8 package or range loop
func firstRune(s string) (rune, bool) {
for _, r := range s {
return r, true
}
return 0, false
}
A Systematic Troubleshooting Process
When you encounter this panic, follow a structured debugging workflow to identify the root cause quickly:
- Read the panic message carefully. It tells you the invalid index and the collection length. For example,
index out of range [4] with length 3means you tried to access element 4 (the 5th element) in a slice of only 3 elements. - Locate the exact line from the stack trace printed alongside the panic. The Go runtime provides file name and line number.
- Trace the index value. Determine how the index was calculated. Is it a loop counter, a function parameter, or a dynamically computed value?
- Add guard conditions. Before the indexing operation, insert a length check or bounds validation.
- Consider nil slices. A nil slice has length 0, so indexing it panics just like an empty slice.
- Write a unit test that reproduces the panic with the exact input that triggered it, then fix the code and verify the test passes.
Prevention Techniques and Defensive Patterns
The best way to fix this panic is to prevent it from happening in the first place. Here are battle-tested patterns used in production Go codebases:
1. Length Guard Clause
Always check the slice length before indexing, especially at function boundaries:
func processRecord(records []Record, index int) (*Record, error) {
if index < 0 || index >= len(records) {
return nil, fmt.Errorf("index %d out of bounds: slice has %d elements", index, len(records))
}
return &records[index], nil
}
2. Use the Range Operator
When you need to iterate over all elements, range eliminates index management entirely and never goes out of bounds:
// Instead of manual indexing
for i := 0; i < len(items); i++ {
process(items[i])
}
// Use range — safer and more idiomatic
for _, item := range items {
process(item)
}
3. Helper Functions for Safe Access
Create reusable utility functions that encapsulate bounds checking:
// SafeSliceIndex returns the element at index i, or a zero value if out of bounds.
func SafeSliceIndex[T any](s []T, i int) T {
var zero T
if i < 0 || i >= len(s) {
return zero
}
return s[i]
}
// SafeSliceRange returns s[low:high] if valid, otherwise nil and an error.
func SafeSliceRange[T any](s []T, low, high int) ([]T, error) {
if low < 0 || high < 0 || low > len(s) || high > len(s) || low > high {
return nil, fmt.Errorf("invalid range [%d:%d] for slice of length %d", low, high, len(s))
}
return s[low:high], nil
}
4. Validate Input at API Boundaries
When your function receives indices as arguments from callers (especially in HTTP handlers or gRPC services), validate them early and return clear error messages:
func GetItemByID(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(r.URL.Query().Get("id"))
if err != nil || id < 0 || id >= len(database) {
http.Error(w, "invalid or out-of-range ID", http.StatusBadRequest)
return
}
item := database[id]
json.NewEncoder(w).Encode(item)
}
5. Leverage recover() for Graceful Degradation
In rare cases where you must protect against panics in third-party libraries or deeply nested calls, use recover() to convert the panic into an error. Use this sparingly — it should not replace proper bounds checking:
func safeCall(fn func()) (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("recovered from panic: %v", r)
}
}()
fn()
return nil
}
6. Static Analysis and Linters
Tools like go vet and third-party linters can catch some index-related issues at compile time. For example, staticcheck can flag loop bounds that are provably incorrect. Integrate these into your CI pipeline:
go vet ./...
staticcheck ./...
Debugging with Delve and Print Statements
When the panic occurs in a complex code path, insert diagnostic prints just before the suspect index operation to capture the slice length and the index value at runtime:
func suspectFunction(data []int, idx int) {
fmt.Printf("DEBUG: len=%d, cap=%d, idx=%d\n", len(data), cap(data), idx)
// Now the panic line will be right after this print
value := data[idx]
fmt.Println(value)
}
For deeper investigation, use the Delve debugger (dlv) to set breakpoints and inspect variables interactively:
dlv debug ./your-program
(dlv) break main.go:42
(dlv) continue
(dlv) print len(data)
(dlv) print idx
Writing Tests to Catch Index Errors
Table-driven tests are perfect for systematically verifying bounds handling. Write tests that probe edge cases — empty slices, negative indices, indices equal to length, and indices beyond length:
func TestSafeAccess(t *testing.T) {
tests := []struct {
name string
slice []int
index int
wantOk bool
wantVal int
}{
{"valid access", []int{10, 20, 30}, 1, true, 20},
{"empty slice", []int{}, 0, false, 0},
{"negative index", []int{10, 20}, -1, false, 0},
{"index equals length", []int{10, 20}, 2, false, 0},
{"index beyond length", []int{10, 20}, 100, false, 0},
{"nil slice", nil, 0, false, 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
val, ok := safeGet(tt.slice, tt.index)
if ok != tt.wantOk || val != tt.wantVal {
t.Errorf("safeGet(%v, %d) = (%v, %v), want (%v, %v)",
tt.slice, tt.index, val, ok, tt.wantVal, tt.wantOk)
}
})
}
}
Real-World Example: Fixing a Panic in a CSV Parser
Imagine you have a CSV parsing function that reads lines and splits them into fields. A panic occurs when a row has fewer columns than expected:
// Original buggy code
func parseRows(rows [][]string) []Person {
var people []Person
for _, row := range rows {
person := Person{
Name: row[0],
Email: row[1], // panics if row has only 1 field
Phone: row[2], // panics if row has fewer than 3 fields
}
people = append(people, person)
}
return people
}
Here is the corrected version with proper validation:
func parseRows(rows [][]string) ([]Person, error) {
var people []Person
for i, row := range rows {
if len(row) < 3 {
return nil, fmt.Errorf("row %d: expected at least 3 fields, got %d: %v", i, len(row), row)
}
person := Person{
Name: row[0],
Email: row[1],
Phone: row[2],
}
people = append(people, person)
}
return people, nil
}
This version returns a descriptive error instead of crashing, making the caller responsible for deciding how to handle malformed data — whether to log it, skip the row, or abort the entire operation.
Best Practices Summary
- Never assume a slice is non-empty. Check
len(s) > 0before accessings[0]. - Prefer range loops over manual index iteration whenever you need all elements.
- Validate indices at function boundaries, especially in public API functions that receive index parameters from callers.
- Return errors, not panics for recoverable out-of-bounds situations. Reserve panics for truly unrecoverable programming errors.
- Write tests that cover edge cases: nil slices, empty slices, negative indices, indices equal to length, and indices beyond length.
- Use linters and go vet to catch potential issues before runtime.
- When working with strings, understand that indexing operates on bytes. Use
rangeor theunicode/utf8package for rune-level access. - Protect shared slices with synchronisation primitives in concurrent code.
- Read the panic message thoroughly — it gives you the exact index and slice length, which is your fastest path to a fix.
Conclusion
The panic: runtime error: index out of range message is a signal that your program has violated Go's memory safety guarantees by attempting to access memory outside the bounds of a slice or array. While the panic itself is abrupt, the error message is descriptive and points you directly to the problem. The fix invariably involves adding a bounds check, switching to a safer iteration pattern, or validating input before it reaches the indexing operation. By adopting defensive coding habits — checking lengths, preferring range loops, validating inputs early, and writing thorough tests — you can eliminate this class of panic from your Go programs entirely. When the panic does occur in production, the structured troubleshooting approach outlined here will help you diagnose and resolve it quickly, turning a crash into a straightforward code improvement.