37. `panic` and `recover`
Go does have a way to blow up the stack. It's called panic, it's reserved
for situations that should never happen, and the most important thing to
learn about it is when not to use it.
What a panic does
A panic stops the normal flow, runs every deferred call on the way out, and crashes the program with a stack trace.
package main
import "fmt"
func main() {
defer fmt.Println("deferred calls still run")
fmt.Println("before")
panic("something went badly wrong")
}
Run it: you get before, then the deferred line, then the panic message. The
deferred calls firing on the way out is what makes cleanup reliable even when
things go wrong — and what makes recover possible.
The runtime panics on your behalf too: index out of range, nil map write, nil pointer dereference, integer divide by zero.
package main
import "fmt"
func main() {
defer fmt.Println("cleanup ran")
nums := []int{1, 2, 3}
i := 10
fmt.Println("about to index out of range...")
fmt.Println(nums[i])
}
recover stops the unwinding
recover is only useful inside a deferred function. Called there during
a panic, it returns the panic value and normal execution resumes in the
caller:
package main
import "fmt"
func risky() {
defer func() {
if r := recover(); r != nil {
fmt.Println("recovered from:", r)
}
}()
fmt.Println("starting risky work")
panic("kaboom")
}
func main() {
risky()
fmt.Println("main is still running")
}
main continues. Note the shape — it's always this shape:
defer func() {
if r := recover(); r != nil {
// handle it
}
}()
Called anywhere other than a deferred function, recover() returns nil and
does nothing.
Turning a panic into an error
The one genuinely idiomatic use of recover: at a package boundary, convert
a panic into a normal error so callers never see it.
package main
import (
"fmt"
"strings"
)
func parse(input string) (result int, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("parse %q: %v", input, r)
}
}()
parts := strings.Split(input, "+")
if len(parts) != 2 {
panic("expected exactly one +")
}
digits := map[string]int{"one": 1, "two": 2, "three": 3}
a, ok := digits[parts[0]]
if !ok {
panic("unknown word: " + parts[0])
}
b, ok := digits[parts[1]]
if !ok {
panic("unknown word: " + parts[1])
}
return a + b, nil
}
func main() {
for _, in := range []string{"one+two", "one+seven", "nonsense"} {
n, err := parse(in)
if err != nil {
fmt.Println("error:", err)
continue
}
fmt.Printf("%s = %d\n", in, n)
}
}
The named result err is what makes this work: the deferred closure runs
after return has picked a value, and assigning to err changes what the
caller receives. Same mechanism as deferred error wrapping in lesson 2.
This is how encoding/json and several parsers are built internally —
panic through deep recursive code to avoid threading an error through every
frame, then recover at the public function and hand back an error. Note
that the panics stay inside the package. Users of parse see only
errors.
The Must convention
The mirror image: a helper that panics on failure, used only where a failure means the program is broken and can't sensibly continue.
package main
import (
"fmt"
"regexp"
"strconv"
)
func MustAtoi(s string) int {
n, err := strconv.Atoi(s)
if err != nil {
panic(fmt.Sprintf("MustAtoi(%q): %v", s, err))
}
return n
}
var pattern = regexp.MustCompile(`^[a-z]+$`)
func main() {
fmt.Println(MustAtoi("42") + 1)
fmt.Println(pattern.MatchString("golang"), pattern.MatchString("Go1"))
}
regexp.MustCompile, template.Must and friends exist because a hard-coded
regex that doesn't compile is a bug, discovered at startup, not a runtime
condition to handle. That's the test for Must...: only for values known at
compile time, in package-level variables or init.
When to panic, and when not to
Panic when the program cannot continue meaningfully:
- an invariant your own code guarantees has been violated ("unreachable")
- required configuration is missing at startup
- a
Must...helper with a hard-coded argument
Return an error for everything else — and "everything else" is nearly everything:
- bad user input
- a file that isn't there
- a network call that failed
- anything a caller could reasonably want to handle
package main
import (
"errors"
"fmt"
)
func divideBad(a, b int) int {
if b == 0 {
panic("division by zero")
}
return a / b
}
func divideGood(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
func main() {
if q, err := divideGood(10, 0); err != nil {
fmt.Println("handled cleanly:", err)
} else {
fmt.Println(q)
}
fmt.Println(divideGood(10, 2))
fmt.Println(divideBad(10, 2))
}
A library that panics on bad input takes the choice away from its caller. That's why the standard library almost never does it.
Two more rules worth knowing:
- Don't recover just to keep going. Recovering and continuing as if nothing happened leaves your program in an unknown state. Recover to convert, log, and return — or don't recover at all.
- A panic in a goroutine kills the whole program, even if the goroutine that started it has a recover. Each goroutine needs its own deferred recover if it might panic. That comes up again in the next module.
Your turn
Complete safeDivide so a division-by-zero panic is recovered and returned
as an error:
5 <nil>
0 recovered: runtime error: integer divide by zero
package main
import "fmt"
func safeDivide(a, b int) (result int, err error) {
// defer a closure that recovers and sets err to
// fmt.Errorf("recovered: %v", r)
return a / b, nil
}
func main() {
q, err := safeDivide(10, 2)
fmt.Println(q, err)
q, err = safeDivide(10, 0)
fmt.Println(q, err)
}
package main
import "fmt"
func safeDivide(a, b int) (result int, err error) {
defer func() {
if r := recover(); r != nil {
result = 0
err = fmt.Errorf("recovered: %v", r)
}
}()
return a / b, nil
}
func main() {
q, err := safeDivide(10, 2)
fmt.Println(q, err)
q, err = safeDivide(10, 0)
fmt.Println(q, err)
}
You can now fail well. Next module: *, &, and what actually lives where —
pointers and memory.