Fix: Go panic: runtime error: index out of range
Part of: Go, Rust & Systems Errors
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. Slicing follows similar rules but uses a half-open interval: s[low:high] requires 0 <= low <= high <= cap(s). The newer three-index form s[low:high:max] (since Go 1.2) also requires high <= max <= cap(s), and violating that produces the same “slice bounds out of range” 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. The compiler is allowed to elide a bounds check when it can prove statically that the index is in range (bounds-check elimination, or BCE), but it does so conservatively. If you see this panic, the runtime check fired — there is no “undefined behavior” silent corruption to chase.
The exact wording of the message also tells you what to look for. index out of range [N] with length M is a single-index access where N >= M (or N < 0). slice bounds out of range [a:b] is a two- or three-index slice expression, and the bracketed pair is the violating segment. Since Go 1.14 (Feb 2020), the runtime prints the offending index and length together — older versions only said index out of range, so if you are reading a stack trace from an ancient binary, expect less detail.
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.
- Aliasing after
append. A slice you saved a reference to may shrink (in length) or even point to reallocated backing memory when another reference to the same underlying array gets re-sliced.
Version History That Changes the Failure Mode
Slice bounds checking has been in Go since 1.0, but several Go releases changed either how easy these bugs are to write or how easy they are to find. Knowing your Go version helps decide whether to reach for newer tooling or stick with manual guards:
- Go 1.14 (Feb 2020) — asynchronous preemption and clearer panic messages. Before 1.14, goroutines could only be preempted at function-call boundaries. Tight loops that scribbled across a shared slice without yielding never gave the scheduler a chance to interleave, which masked many concurrency races. After 1.14, tight loops can be preempted mid-iteration, and a whole class of “worked yesterday, panics today” index bugs surfaced. The release also reformatted runtime error messages to include the index and length (
[3] with length 3), which is the format most current articles assume. - Go 1.17 (Aug 2021) — register-based calling convention. Stack traces from runtime panics began showing register-passed arguments slightly differently. If your old grep patterns over panic logs broke around this release, this is why.
- Go 1.18 (Mar 2022) — generics. Type parameters let you write safe slice helpers (
First[T],Last[T],At[T]) once instead of duplicating per type. Before 1.18, the idiomatic pattern wasinterface{}plus runtime type assertions or hand-rolled per-type helpers, both of which discouraged centralizing the bounds check. - Go 1.21 (Aug 2023) —
min,max,clearbuilt-ins;slicesandmapspackages graduated fromx/exp.clear(s)zeroes a slice without re-allocating.slices.Index,slices.Contains,slices.Delete, andslices.Inserthandle bounds correctly, so reaching for them is almost always safer than manual indexing. - Go 1.22 (Feb 2024) — loop variable scoping change. Each iteration of a
forloop now gets its own variable. Code that captured&items[i]in a closure used to share one address across iterations; under 1.22, every closure gets its own. If you maintained a manual offset that depended on the old shared semantics, the migration can introduce off-by-one index bugs you did not have before. golang.org/x/exp/sliceshistory. Theslicespackage lived underx/expfrom 2021 until being absorbed into the standard library in 1.21. APIs were renamed during the move (SortFuncsignature changed in particular). If you copy-paste examples from old blog posts, double-check the signatures against the 1.21+ standard library.
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])
}Check for backing-array aliasing after append. If you took a sub-slice and saved it, later append calls on the parent may reallocate, leaving you with a stale view, or may extend into the parent’s territory:
parent := make([]int, 0, 8)
parent = append(parent, 1, 2, 3)
child := parent[1:3] // child points into parent's backing array
parent = append(parent, 4, 5, 6, 7, 8, 9) // may reallocate
_ = child[2] // length is 2, panicsIf the slice you read is computed elsewhere, copy it (child := append([]int(nil), parent[1:3]...)) to detach from the parent.
Check for unsafe.Slice and CGo boundaries. unsafe.Slice(ptr, n) builds a slice from a C pointer without any guarantees about the underlying allocation. The runtime bounds check still fires for normal indexing, but if n was computed from untrusted input, you can panic on the first access. Validate n against the real C-side allocation before the call.
Check assembly or compiler-generated stubs. Bounds checks generated by the compiler reference the source line of the indexing expression, not the function that computed the index. If the printed line points to a one-liner like return s[i], the real bug is wherever i was computed, often several frames up the stack.
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. For Node.js heap-related panics that look similar in failure mode, see Fix: JavaScript heap out of memory.
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.