Fix: Go panic: runtime error: index out of range
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 3Or variations:
panic: runtime error: index out of range [0] with length 0panic: 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 +0x1aYou 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 oflen(slice)-1. - Wrong loop bounds. Looping with
i <= len(slice)instead ofi < 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 3Fixed:
last := items[len(items)-1] // Index 2 — the last elementBroken — 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 aforloop. In Go (and most languages), the last valid index islen(slice) - 1. Use<not<=for the loop condition, or better yet, userange.
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 sliceGuard 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 5Always 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)/2Check 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.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Go Test Not Working — Tests Not Running, Failing Unexpectedly, or Coverage Not Collected
How to fix Go testing issues — test function naming, table-driven tests, t.Run subtests, httptest, testify assertions, and common go test flag errors.
Fix: Go Generics Type Constraint Error — Does Not Implement or Cannot Use as Type
How to fix Go generics errors — type constraints, interface vs constraint, comparable, union types, type inference failures, and common generic function pitfalls.
Fix: Go Deadlock — all goroutines are asleep, deadlock!
How to fix Go channel deadlocks — unbuffered vs buffered channels, missing goroutines, select statements, closing channels, sync primitives, and detecting deadlocks with go race detector.
Fix: Go Error Handling Not Working — errors.Is, errors.As, and Wrapping
How to fix Go error handling — errors.Is vs ==, errors.As for type extraction, fmt.Errorf %w for wrapping, sentinel errors, custom error types, and stack traces.