panic: runtime error: index out of range - 11 Causes and Fixes in Go

panic: runtime error: index out of range means you asked for an element at a position that does not exist, and Go killed the goroutine rather than hand you garbage. Modern Go appends the specifics, so you usually see something like panic: runtime error: index out of range [3] with length 3 or panic: runtime error: index out of range 0 with length 0 - and those two numbers are the whole diagnosis.

The index is what your code asked for. The length is what the slice, array, or string actually had. If the index equals the length, you have an off-by-one in a loop bound. If the length is 0, the collection was empty or nil and the real bug is upstream, in whatever was supposed to fill it. If the index is negative, you fed a not-found sentinel like -1 straight into a subscript.

Below are 11 distinct causes, most common first, each with a reproduction you can paste into main.go and the fix that differs from it by the lines that matter. After that, a diagnostic checklist for the case that matches none of them, and the design decision behind the panic so you recognise the whole family next time. Focused view of a computer screen displaying code and debug information.

Errorpanic: runtime error: index out of range [3] with length 3
Where it happensGo - any version, any platform. Raised by the runtime at the indexing expression, in the goroutine that executed it. Older Go releases printed only index out of range; current releases append the index and the length.
What it meansYou indexed a slice, array, or string at a position that is at or past its length (or below zero), and Go refuses to read memory it cannot prove belongs to that value.

The Fast Fix

Check the length before you index. The overwhelming majority of these panics are an index that equals the length, or an index into something empty:

// before: panics when rows is empty or shorter than expected
return rows[0]

// after
if len(rows) == 0 {
    return "", fmt.Errorf("no rows")
}
return rows[0], nil

If the panic came from a loop, the bound is almost certainly <= where it should be < - or better, drop the counter entirely and use for i, v := range s, which cannot go out of range.

If the message says length 0, stop looking at the index. Something that was supposed to populate the collection returned nothing, and that is the actual bug.

What Is Actually Causing It

1. Loop condition uses <= instead of <

Reproduce it

s := []string{"a", "b", "c"}
for i := 0; i <= len(s); i++ {
    fmt.Println(s[i])
}
// panic: runtime error: index out of range [3] with length 3

Why it happens — Valid indices run from 0 to len(s)-1. i <= len(s) lets the loop body run one extra time with i == len(s), which is exactly one past the last element. This is why the index in the panic message equals the length.

The fix

s := []string{"a", "b", "c"}
for _, v := range s {
    fmt.Println(v)
}

Changed: the manual counter is gone. If you need the index, for i := range s is also bounded automatically; if you must keep a counter, the condition is i < len(s).

Confirm it worked — Run the function with a 1-element and a 0-element slice. Both must complete without a panic - a <= bound fails on any non-empty input, so a passing empty case alone proves nothing.


2. Indexing a nil or empty slice with no length check

Reproduce it

func firstRow(rows []string) string {
    return rows[0]
}

func main() {
    var rows []string // nil slice, len 0
    fmt.Println(firstRow(rows))
}
// panic: runtime error: index out of range [0] with length 0

Why it happens — A nil slice is a perfectly valid empty slice: len, cap, range, and append all work on it. Indexing does not. The runtime compares 0 < len(rows), which is 0 < 0, and panics. This is the index out of range 0 with length 0 variant.

The fix

func firstRow(rows []string) (string, error) {
    if len(rows) == 0 {
        return "", fmt.Errorf("firstRow: empty input")
    }
    return rows[0], nil
}

Changed: the length guard, and the signature now reports the empty case instead of crashing on it. Never test rows != nil here - an empty-but-non-nil slice ([]string{}) passes that check and still panics.

Confirm it worked — Add t.Run("empty", ...) passing both nil and []string{}. Both must return the error, not panic. go test ./... surfaces a panic as a failed test with the stack trace.


3. strings.Split result assumed to have N parts

Reproduce it

line := "name" // this row has no comma
parts := strings.Split(line, ",")
fmt.Println(parts[1])
// panic: runtime error: index out of range [1] with length 1

Why it happens — strings.Split never fails and never returns nil for a non-empty input - when the separator is absent it returns a single-element slice containing the whole string. Code that assumes two or more fields breaks on the first malformed line, which in log and CSV parsing is usually a blank line or a trailing newline.

The fix

line := "name"
parts := strings.SplitN(line, ",", 2)
if len(parts) < 2 {
    return fmt.Errorf("malformed line %q: want key,value", line)
}
fmt.Println(parts[1])

Changed: the arity check before the index, and SplitN caps the split so parts[1] is the rest of the line rather than only the second field. The same applies to strings.Fields on a whitespace-only line, which returns a zero-length slice.

Confirm it worked — Feed the parser a file whose last line is empty and a line with no separator. Both should produce your error message. Logging len(parts) and the quoted line is enough to identify the offending input.


4. os.Args read without checking the argument count

Reproduce it

func main() {
    path := os.Args[1]
    fmt.Println("reading", path)
}
// $ go run .
// panic: runtime error: index out of range [1] with length 1

Why it happens — os.Args[0] is always the program name, so a run with no arguments gives a slice of length 1. Every positional argument you read is an unchecked index. This one usually escapes testing because the developer always types the argument.

The fix

func main() {
    if len(os.Args) < 2 {
        fmt.Fprintln(os.Stderr, "usage: tool <path>")
        os.Exit(2)
    }
    path := os.Args[1]
    fmt.Println("reading", path)
}

Changed: the count check plus a usage message and a non-zero exit code instead of a stack trace. For more than one or two arguments, use the flag package and flag.Arg(0), which returns "" rather than panicking.

Confirm it worked — Run the binary with no arguments. You should get the usage line and exit status 2 - check with echo $LASTEXITCODE in PowerShell or echo $? in bash.


5. make() given a capacity where a length was meant

Reproduce it

in := []int{1, 2, 3}
out := make([]int, 0, len(in)) // length 0, capacity 3
for i, v := range in {
    out[i] = v * 2
}
// panic: runtime error: index out of range [0] with length 0

Why it happens — make([]T, length, capacity) with a length of 0 returns a slice you cannot index at all, even though the backing array already has room. Capacity is allocation; length is addressability. Indexing is checked against length only, which is why the panic reports length 0 on a slice that clearly has space.

The fix

in := []int{1, 2, 3}
out := make([]int, len(in)) // length 3, indexable at 0..2
for i, v := range in {
    out[i] = v * 2
}

Changed: the length argument. The other correct shape is to keep make([]int, 0, len(in)) and use out = append(out, v*2) - pick one, never mix assignment and append on the same slice.

Confirm it worked — Print fmt.Println(len(out), cap(out)) right after make. For the index-assignment pattern, len must equal the number of slots you intend to write.


6. Regexp submatch indexed when there was no match

Reproduce it

re := regexp.MustCompile(`v(\d+)`)
m := re.FindStringSubmatch("no version here")
fmt.Println(m[1])
// panic: runtime error: index out of range [1] with length 0

Why it happens — FindStringSubmatch returns nil when the pattern does not match - not an empty-string slice, and not an error. Indexing that nil gives length 0. Even on a successful match, an optional group that did not participate yields an empty string rather than shrinking the slice, so the length check and the emptiness check are different questions.

The fix

re := regexp.MustCompile(`v(\d+)`)
m := re.FindStringSubmatch("no version here")
if m == nil {
    return fmt.Errorf("no version found")
}
fmt.Println(m[1]) // group 1 is guaranteed present when m != nil

Changed: the nil check. Once m is non-nil its length is 1 + re.NumSubexp(), so group indices up to that count are safe without further checks.

Confirm it worked — Call the function with a string you know does not match. It must return the error. re.NumSubexp() printed once at startup confirms how many group indices are legal.


More causes (5 remaining)

7. A not-found sentinel of -1 used as an index

Reproduce it

addr := "user@example.com"
i := strings.Index(addr, "/") // -1: not found
fmt.Println(addr[i:])
// panic: runtime error: index out of range [-1]

Why it happens — Go's search functions - strings.Index, strings.LastIndex, bytes.IndexByte - report absence with -1, and the runtime rejects negative subscripts just as firmly as oversized ones. A negative index in the panic message is almost always an unchecked return value from a search, not arithmetic gone wrong.

The fix

addr := "user@example.com"
i := strings.Index(addr, "/")
if i < 0 {
    return "", fmt.Errorf("no path separator in %q", addr)
}
fmt.Println(addr[i:])

Changed: the i < 0 check before use. Where it fits, prefer strings.Cut, which returns a found boolean and both halves, so there is no index to mishandle.

Confirm it worked — Grep your package for strings.Index, strings.LastIndex, and bytes.Index and confirm every result is compared against 0 or -1 before it reaches a subscript or slice expression.


8. Byte length of a string used to index its runes

Reproduce it

s := "héllo"
runes := []rune(s)
for i := 0; i < len(s); i++ { // len(s) is 6 bytes
    fmt.Print(string(runes[i]))
}
// panic: runtime error: index out of range [5] with length 5

Why it happens — len on a string counts bytes, not characters. é is two bytes in UTF-8, so a 5-character string has length 6 and the rune slice has length 5. The loop bound and the collection being indexed are measuring different things.

The fix

s := "héllo"
for _, r := range s { // ranging a string yields runes
    fmt.Print(string(r))
}

Changed: the loop ranges over the string directly, decoding one rune per iteration. If you genuinely need random access by character, keep runes := []rune(s) and bound the loop with len(runes).

Confirm it worked — Run the function on a non-ASCII input such as "héllo" or an emoji. An ASCII-only test case cannot catch this, because there bytes and runes are 1:1.


9. Two parallel slices assumed to be the same length

Reproduce it

names := []string{"ana", "bo", "cy"}
scores := []int{10, 20} // one short
for i := range names {
    fmt.Println(names[i], scores[i])
}
// panic: runtime error: index out of range [2] with length 2

Why it happens — The loop is bounded by one collection and indexes another. Nothing in the type system ties their lengths together, so any upstream filter, failed lookup, or skipped row silently desynchronises them and the longer slice drives the index past the shorter one's end.

The fix

type player struct {
    name  string
    score int
}

players := []player{{"ana", 10}, {"bo", 20}, {"cy", 0}}
for _, p := range players {
    fmt.Println(p.name, p.score)
}

Changed: the two slices became one slice of structs, so there is no second length to get wrong. If you cannot restructure, assert the invariant once at the boundary: if len(names) != len(scores) { return error }.

Confirm it worked — Log len(names) and len(scores) where the data is assembled, not where it is consumed. Equal values at the producer prove the invariant holds before anything indexes.


10. Elements removed while iterating with a precomputed bound

Reproduce it

items := []int{1, 2, 3, 4}
n := len(items) // bound captured before the loop
for i := 0; i < n; i++ {
    if items[i]%2 == 0 {
        items = append(items[:i], items[i+1:]...) // shrinks items
    }
    fmt.Println(items[i])
}
// panic: runtime error: index out of range [2] with length 2

Why it happens — The bound n is a snapshot of the length taken before any deletion. Each append-based removal shortens items, so the loop keeps walking toward a length that no longer exists. The same trap applies to for i := range items, where the range expression is evaluated once.

The fix

items := []int{1, 2, 3, 4}
kept := items[:0] // reuse the backing array
for _, v := range items {
    if v%2 != 0 {
        kept = append(kept, v)
    }
}
items = kept
fmt.Println(items)

Changed: filtering now writes forward into a separate length instead of mutating the slice being iterated. If you must delete in place, iterate backwards (for i := len(items) - 1; i >= 0; i--) so removals only affect indices you have already passed.

Confirm it worked — Run the filter on an input where every element is removable, and one where only the last is. Both must finish and leave the expected len(items).


11. Shared slice appended from multiple goroutines

Reproduce it

var results []int
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
    wg.Add(1)
    go func(v int) {
        defer wg.Done()
        results = append(results, v) // unsynchronised: a data race
    }(i)
}
wg.Wait()
fmt.Println(results[99])
// panic: runtime error: index out of range [99] with length 87 (count varies)

Why it happens — append reads the slice header, writes an element, and writes the header back. Concurrent appends interleave those steps and overwrite each other's length, so elements are lost and the final length is nondeterministic. The panic is a symptom; the race is the bug, and it will also corrupt data without panicking.

The fix

results := make([]int, 100) // length fixed up front
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
    wg.Add(1)
    go func(v int) {
        defer wg.Done()
        results[v] = v // each goroutine owns exactly one index
    }(i)
}
wg.Wait()
fmt.Println(results[99])

Changed: the slice is preallocated to its final length and each goroutine writes a distinct index, so no header is mutated and no lock is needed. If the result count is not known in advance, send values over a channel and append in a single collector goroutine, or guard the append with a sync.Mutex.

Confirm it worked — go run -race . or go test -race ./.... The race detector names the two conflicting goroutines and the exact line; a clean -race run on the fixed version is the proof.

High-resolution image of colorful programming code highlighted on a computer screen. ## None of Those? Narrow It Down
  1. Read the two numbers in the panic message before touching any code. Index equals length means an off-by-one loop bound; length 0 means the collection was never populated; a negative index means an unchecked -1 sentinel. Each rules out the other two.
  2. Find the topmost frame in the stack trace that is your own package, not runtime.*. That file.go:line is the exact indexing expression - the runtime frames above it are just the panic machinery.
  3. Log len() and cap() on one line immediately before the index. If len is 0 and cap is non-zero, you have the make length-vs-capacity bug; if both are 0, the bug is in whatever was supposed to fill the slice, not here.
  4. Trace where the index came from. A parse or search result (strings.Index, FindStringSubmatch, a map lookup) means a not-found contract was ignored. A loop counter means the bound is wrong. A second collection's length means parallel slices have desynchronised.
  5. If the bound and the collection are different values - len(s) versus len([]rune(s)), len(names) versus len(scores) - assert they are equal at that point. A failed assertion converts an intermittent panic into a deterministic one.
  6. Run go test -race ./.... If the panic only appears under load, in CI, or with varying index values, a concurrent append or write is reordering lengths behind you.
  7. If nothing reproduces, add the real input to a table-driven test with empty, single-element, ragged, and non-ASCII cases, then widen with go test -fuzz on the parsing function. Most surviving instances of this panic are an input shape nobody wrote a case for.

Why This Error Exists At All

Go bounds-checks every index it cannot prove safe at compile time, and that is a deliberate trade. In C, reading one past the end of an array is undefined behaviour: it might return adjacent memory, silently corrupt a neighbouring variable, or hand an attacker a read primitive. Go's authors decided a loud, immediate, unambiguous crash at the offending line is cheaper than a corrupted process that fails somewhere else an hour later. The spec requires the panic; it is not a debug-build nicety you can switch off. The compiler does eliminate checks it can prove redundant - a for i := range s loop indexing s[i], for instance - so the cost you pay is for the accesses it cannot reason about, which are exactly the ones that are actually dangerous.

The second half of the design is the slice header: a pointer, a length, and a capacity. Indexing is checked against length only, never capacity, which is why make([]int, 0, 1000) panics on s[0] despite a thousand allocated slots. Capacity tells append when it can grow without reallocating; length tells the runtime which slots are yours to read. Keeping them separate is what makes append amortised-cheap and sub-slicing free, and the price is that "has room" and "has elements" are two different properties you have to keep straight. The nil slice follows from the same choice: it is a header with a nil pointer and length 0, which is a legitimate empty slice for len, range, and append, and an invalid one for indexing. That is also why s != nil is never the right guard - len(s) > 0 is.

Once you see it as "length is the only thing checked", the entire family becomes one error. index out of range is a subscript at or past the length. slice bounds out of range is a slice expression s[a:b] that exceeded length or capacity - a different message precisely because slicing is allowed to reach up to cap, one place where the two numbers genuinely diverge. invalid memory address or nil pointer dereference is the same philosophy applied to pointers. And unlike Python's catchable IndexError or JavaScript's silent undefined, Go gives you neither a routine exception to swallow nor a bogus value to propagate. The message hands you the index and the length because, with those two numbers and the top stack frame, the bug is usually located before you have finished reading them. Two engineers collaborate on prosthetic design using a laptop, focusing on technological innovation.

Stop It From Coming Back

  • Make the length check part of the function's contract: any function that indexes a slice it did not create should either take a guaranteed-non-empty input or return an error for the empty case. An accessor returning (T, bool) is cheaper to call correctly than a documented precondition nobody reads.
  • Default to for i, v := range s and reserve explicit counters for genuine stride or reverse iteration. Range bounds cannot drift past the length, which removes the entire off-by-one class mechanically rather than by review.
  • Enable golangci-lint in CI with go vet and staticcheck. They catch constant out-of-range indexes on arrays, suspicious loop shapes, and ignored results from search functions before the code ever runs.
  • Write table-driven tests whose cases always include empty input, a single element, a ragged or short row, and a non-ASCII string. These four shapes account for most of the causes above, and they cost one line each in a []struct{...} table.
  • Run go test -race ./... in CI, not just locally. Index panics that vary between runs are almost always a concurrent write to a shared slice, and the race detector points at the line while a stack trace alone does not.
  • Parse structured input with the library that enforces arity instead of hand-splitting: encoding/csv with Reader.FieldsPerRecord rejects ragged rows at read time, strings.Cut removes the -1 sentinel, and flag replaces raw os.Args indexing.
  • For long-lived servers, keep a recover() at the request boundary as blast-radius control, and log the recovered value with the stack. Note net/http already recovers panics in handlers so one bad request does not kill the process - which is exactly why these panics can hide in production logs for weeks.

Related Guides

Errors You Will Probably Hit Next

  • panic: runtime error: slice bounds out of range [:5] with capacity 3
  • panic: runtime error: invalid memory address or nil pointer dereference
  • panic: assignment to entry in nil map

Read the index and the length first; between them they name the cause before you open the file. Index equal to length is a loop bound, length 0 is an upstream producer that returned nothing, and a negative index is a not-found sentinel you forgot to check. The durable fix is almost never a longer bounds check at the crash site - it is making the length impossible to get wrong, by ranging instead of counting, by one slice of structs instead of two parallel slices, and by functions that refuse empty input at the boundary.

댓글

이 블로그의 인기 게시물

TypeError: Cannot read properties of undefined (reading 'map') - 11 Causes and Fixes

psql: FATAL: password authentication failed for user - 10 Causes and Fixes

Error: ERR_MODULE_NOT_FOUND: Cannot find package - 10 Causes and Fixes