Right, let’s talk about panic. It’s the moment your Go code throws its hands up in the air and says, “I’m out, you deal with this.” It’s not an error; it’s a full-blown, runtime-stopping tantrum. And the two most common ways you’ll accidentally trigger one are by reaching for something that isn’t there (index out of range) or by trying to use nothing as if it were something (nil dereference). These aren’t gentle reminders; they’re a brick wall.

Think of a slice or an array as a series of numbered mailboxes. mySlice[0] is mailbox number one. mySlice[4] is mailbox number five. Now, if you have a slice with only 5 elements (mailboxes 0 through 4), and you try to fmt.Println(mySlice[5]), you’re asking for the sixth mailbox in a five-mailbox row. The runtime isn’t going to make something up for you. It’s going to panic. It’s the only sane thing to do. The alternative is silently returning garbage data, and we’re not in the business of that.

Similarly, a pointer is a little note that says “the value you’re looking for is over there.” A nil pointer is a note that says “there’s nothing over there, don’t even bother.” When you dereference a pointer with *myPointer or try to access a method or field on a nil struct pointer with myPointer.MyField, you’re saying, “Okay, I’m going to go look where this note says.” If the note says nil, you’re walking straight into a wall. Hence, panic.

The Classic Off-by-One (and Other Indexing Blunders)

This is the bread and butter of runtime panics. It usually happens when you’re manually iterating with indices instead of using range, or when you’re calculating an index based on some flawed logic.

func main() {
    mySlice := []string{"a", "b", "c"}
    
    // This is fine. Perfectly normal.
    fmt.Println(mySlice[0]) // "a"
    fmt.Println(mySlice[2]) // "c"
    
    // This is a very, very bad idea.
    fmt.Println(mySlice[3]) // panic: runtime error: index out of range [3] with length 3
    
    // A more subtle, classic mistake.
    for i := 0; i <= len(mySlice); i++ { // The '<=' is the killer here.
        fmt.Println(mySlice[i]) // Panics on the last iteration when i == 3
    }
}

The fix? Almost always, use range. It’s harder to get wrong. If you must use an index, double-check your boundary condition. Is it < or <=? Is it len(slice) or len(slice)-1? The compiler won’t save you here; this is a runtime check.

The Billion-Dollar Mistake: Nil Dereference

Sir Tony Hoare called inventing the null reference his “billion-dollar mistake,” and honestly, he might have been lowballing it. This panic occurs when you treat nil as a valid reference to a struct, map, slice, or channel.

type MyStruct struct {
    Message string
}

func main() {
    var myStructPointer *MyStruct // declared but not initialized, so it's nil
    
    // This will compile perfectly. The panic happens at runtime.
    fmt.Println(myStructPointer.Message) // panic: runtime error: invalid memory address or nil pointer dereference

    // The same goes for maps and slices. They are nil until initialized with make() or a literal.
    var myMap map[string]int
    myMap["key"] = 42 // panic: assignment to entry in nil map

    var mySlice []int
    mySlice[0] = 1 // panic: runtime error: index out of range [0] with length 0
}

The defense here is proactive. If a function can return a nil pointer, you must check for it before you use it. It’s not optional.

Why Panic is Actually a Good Thing (Seriously)

This might sound counterintuitive, but a panic is better than the alternative: silent corruption. In languages without such mechanisms, accessing invalid memory might just return whatever garbage value was at that memory address. Your program would continue running, confidently calculating nonsense. A panic stops the show immediately. It’s a brutal but effective form of fail-fast. It tells you, loudly and clearly, that your program’s logic is broken and its assumptions are wrong. You want to know this as soon as it happens.

The Pitfall of Recovering from Panic

Go gives you a trapdoor called recover(). It’s a mechanism to catch a panic that’s unwinding the stack from within a defer function. It exists so you can prevent a panic from crashing your entire application—useful for things like web servers where you want one bad request to fail gracefully without killing the server.

But here’s the catch, and it’s a big one: recover() only works inside a defer. And not just any defer—a defer that’s called during the panic sequence. You can’t just wrap a line of code in a recover. The mental model is to think of defer and recover as a pair.

func thisWillPanic() {
    panic("oh no")
}

func safeCaller() {
    // The defer is crucial. This function is set up *before* the panic happens.
    defer func() {
        if r := recover(); r != nil { // r is the value passed to panic()
            fmt.Println("Recovered from panic:", r)
            // Here you can log the error, send a 500 HTTP response, etc.
        }
    }()
    
    thisWillPanic() // This triggers the panic
    fmt.Println("This will never print") // Execution stops at the panic
}

func main() {
    safeCaller()
    fmt.Println("The main function continues happily.") // This will print
}

The questionable choice here is the design of recover itself. It’s a powerful tool, but its behavior is entirely dependent on the context of defer, which can be confusing. Overusing recover is a bad idea. It’s for strategic, application-level boundaries, not for catching routine errors. If you can handle a condition with an if err != nil, you absolutely should. Reserve panic and recover for truly exceptional, unrecoverable-at-that-level situations.