Skip to content

Fix: Go panic: runtime error: index out of range

FixDevs ·

Quick Answer

How to fix Go panic runtime error index out of range caused by empty slices, off-by-one errors, nil slices, concurrent access, and missing bounds checks.

The Error

Your Go program crashes with:

panic: runtime error: index out of range [3] with length 3

Or variations:

panic: runtime error: index out of range [0] with length 0
panic: runtime error: index out of range [-1]
panic: runtime error: slice bounds out of range [5:3]
goroutine 1 [running]:
main.main()
	/app/main.go:15 +0x1a

You accessed a slice or array element at an index that does not exist. Go panics at runtime because it performs bounds checking on every array/slice access.

Why This Happens

Go slices and arrays are zero-indexed. A slice of length 3 has valid indices 0, 1, and 2. Accessing index 3 or higher, or a negative index, triggers a panic.

Unlike C, Go does not allow out-of-bounds memory access. Every index operation is bounds-checked at runtime. This prevents memory corruption but causes panics if your code has index bugs.

Common causes:

  • Empty slice. Accessing slice[0] when the slice has no elements.
  • Off-by-one error. Using len(slice) as an index instead of len(slice)-1.
  • Wrong loop bounds. Looping with i <= len(slice) instead of i < len(slice).
  • Missing length check. Not verifying the slice has enough elements before accessing.
  • Concurrent modification. Another goroutine shrinks the slice while you access it.
  • Nil slice. A nil slice has length 0, so any index access panics.

Fix 1: Check Length Before Accessing

Always verify the slice has enough elements:

Broken:

func getFirst(items []string) string {
    return items[0]  // Panics if items is empty!
}

Fixed:

func getFirst(items []string) string {
    if len(items) == 0 {
        return ""  // Return zero value for empty slice
    }
    return items[0]
}

For accessing any index:

func getAt(items []string, index int) (string, bool) {
    if index < 0 || index >= len(items) {
        return "", false
    }
    return items[index], true
}

// Usage:
value, ok := getAt(mySlice, 5)
if ok {
    fmt.Println(value)
}

Pro Tip: Follow the Go pattern of returning a value and a boolean (value, ok) for operations that can fail. This mirrors how map lookups and type assertions work in Go and makes error handling explicit.

Fix 2: Fix Off-By-One Errors

The most classic programming error:

Broken — accessing index equal to length:

items := []string{"a", "b", "c"}

// items[3] does NOT exist — valid indices are 0, 1, 2
last := items[len(items)]  // panic: index out of range [3] with length 3

Fixed:

last := items[len(items)-1]  // Index 2 — the last element

Broken — wrong loop condition:

for i := 0; i <= len(items); i++ {  // <= includes len(items), which is out of bounds
    fmt.Println(items[i])
}

Fixed:

for i := 0; i < len(items); i++ {  // < stops before len(items)
    fmt.Println(items[i])
}

// Even better — use range
for i, item := range items {
    fmt.Println(i, item)
}

Use range whenever possible. It handles bounds automatically and is the idiomatic Go way to iterate:

for _, item := range items {
    fmt.Println(item)
}

Common Mistake: Using i <= len(slice) in a for loop. In Go (and most languages), the last valid index is len(slice) - 1. Use < not <= for the loop condition, or better yet, use range.

Fix 3: Fix Slice Bounds in Sub-Slicing

Sub-slice operations also panic on invalid bounds:

data := []int{1, 2, 3, 4, 5}

// Panic: slice bounds out of range [6:5]
sub := data[6:]

// Panic: slice bounds out of range [2:8]
sub := data[2:8]

Fixed — validate bounds:

func safeSlice(data []int, start, end int) []int {
    if start < 0 {
        start = 0
    }
    if end > len(data) {
        end = len(data)
    }
    if start > end {
        return nil
    }
    return data[start:end]
}

Common sub-slice patterns:

// First N elements (capped at length)
func firstN(data []int, n int) []int {
    if n > len(data) {
        n = len(data)
    }
    return data[:n]
}

// Last N elements (capped at length)
func lastN(data []int, n int) []int {
    if n > len(data) {
        n = len(data)
    }
    return data[len(data)-n:]
}

Fix 4: Handle Nil and Empty Slices

A nil slice has length 0. Accessing any index panics:

var items []string  // nil slice
fmt.Println(len(items))  // 0 — safe
fmt.Println(items[0])    // panic!

items = []string{}  // empty (non-nil) slice
fmt.Println(len(items))  // 0 — safe
fmt.Println(items[0])    // panic! Same as nil slice

Guard all index access:

func processItems(items []string) {
    if len(items) == 0 {
        fmt.Println("No items to process")
        return
    }

    first := items[0]
    last := items[len(items)-1]
    fmt.Printf("First: %s, Last: %s\n", first, last)
}

Nil slices are safe for append, len, cap, and range:

var items []string  // nil

items = append(items, "hello")  // Safe — append handles nil
fmt.Println(len(items))          // Safe — returns 0 for nil

for _, item := range items {     // Safe — iterates 0 times for nil
    fmt.Println(item)
}

Fix 5: Fix Map-Based Index Lookups

When you use a map value as an index:

indices := map[string]int{"a": 0, "b": 1, "c": 2}
data := []string{"x", "y", "z"}

// If key doesn't exist, map returns 0 — might be a valid index by accident
idx := indices["d"]       // Returns 0 (zero value), not an error
fmt.Println(data[idx])    // Prints data[0] — wrong but no panic

// Fix: Check if the key exists
idx, ok := indices["d"]
if !ok {
    fmt.Println("Key not found")
    return
}
fmt.Println(data[idx])

When the map value is used for slicing:

offsets := map[string]int{"start": 10}
data := []byte("short")

start := offsets["start"]  // 10
_ = data[start:]           // panic: index out of range [10] with length 5

Always validate map values before using them as indices.

Fix 6: Fix Concurrent Slice Access

Goroutines can cause index panics when one shrinks a slice while another reads it:

Broken:

var data []int

go func() {
    for {
        if len(data) > 0 {
            data = data[1:]  // Shrink the slice
        }
    }
}()

go func() {
    for {
        if len(data) > 0 {
            _ = data[0]  // May panic if other goroutine shrinks data between check and access
        }
    }
}()

Fixed — use a mutex:

var (
    data []int
    mu   sync.Mutex
)

go func() {
    for {
        mu.Lock()
        if len(data) > 0 {
            data = data[1:]
        }
        mu.Unlock()
    }
}()

go func() {
    for {
        mu.Lock()
        if len(data) > 0 {
            _ = data[0]
        }
        mu.Unlock()
    }
}()

Fixed — use channels instead of shared slices:

ch := make(chan int, 100)

// Producer
go func() {
    for _, v := range items {
        ch <- v
    }
    close(ch)
}()

// Consumer
for v := range ch {
    process(v)
}

Fix 7: Recover from Panics

For server applications, recover from panics to avoid crashing the entire process:

func safeHandler(w http.ResponseWriter, r *http.Request) {
    defer func() {
        if r := recover(); r != nil {
            log.Printf("Recovered from panic: %v", r)
            http.Error(w, "Internal Server Error", 500)
        }
    }()

    // Your handler code that might panic
    processRequest(w, r)
}

As middleware:

func recoveryMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if err := recover(); err != nil {
                log.Printf("Panic recovered: %v\n%s", err, debug.Stack())
                http.Error(w, "Internal Server Error", 500)
            }
        }()
        next.ServeHTTP(w, r)
    })
}

Note: recover() only catches panics in the current goroutine. Panics in child goroutines are not caught by the parent’s defer/recover.

Fix 8: Use Helper Functions for Safe Access

Create utility functions for common safe-access patterns:

// Safe get with default value
func getOr[T any](slice []T, index int, defaultVal T) T {
    if index < 0 || index >= len(slice) {
        return defaultVal
    }
    return slice[index]
}

// Usage:
name := getOr(names, 5, "unknown")

// Safe first/last
func first[T any](slice []T) (T, bool) {
    if len(slice) == 0 {
        var zero T
        return zero, false
    }
    return slice[0], true
}

func last[T any](slice []T) (T, bool) {
    if len(slice) == 0 {
        var zero T
        return zero, false
    }
    return slice[len(slice)-1], true
}

Still Not Working?

Use the -race flag to detect concurrent access:

go run -race main.go
go test -race ./...

The race detector finds concurrent slice/map access at runtime and reports it with stack traces.

Check for index arithmetic bugs. Complex index calculations are error-prone:

// Dangerous — what if mid overflows or is negative?
mid := (low + high) / 2

// Safer
mid := low + (high-low)/2

Check for empty function returns. Functions that return slices might return nil or empty slices:

results := fetchResults()  // Might return nil
if len(results) > 0 {
    process(results[0])
}

For Go type mismatch errors, see Fix: Go cannot use X as type Y. For undefined variable errors, see Fix: Go undefined variable. For Go module issues, see Fix: Go module not found.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles