33 Panic and Recover

52 阅读4分钟

原文 golangbot.com/panic-and-r… 本文删除了一些废话 做一下精简

一般用error就够了

啥时候panic?

func panic(interface{})
func recover() interface{}

There are two valid use cases for panic.

  • An unrecoverable error where the program cannot simply continue its execution.  One example is a web server that fails to bind to the required port. In this case, it’s reasonable to panic as there is nothing else to do if the port binding itself fails.
  • A programmer error.  Let’s say we have a method that accepts a pointer as a parameter and someone calls this method using a nil argument. In this case, we can panic as it’s a programmer error to call a method with nil argument which was expecting a valid pointer.

when a funciton has panic, what will happen?

When a function encounters a panic, its execution is stopped, any deferred functions are executed and then the control returns to its caller. This process continues until all the functions of the current goroutine have returned at which point the program prints the panic message, followed by the stack trace and then terminates.

example1. nil pointer
func fullName(firstName *string, lastName *string) {
	defer func() {
		fmt.Println("defer msg from panic func")
	}()

	if firstName == nil {
		panic("runtime error: first name cannot be nil")
	}
	if lastName == nil {
		panic("runtime error: last name cannot be nil")
	}
	fmt.Println("returned normally from fullName")
}

func test() {

	go func() {
		defer func() {
			fmt.Println("defer from another goroutine")
		}()
		fmt.Println("message from another goroutine")
	}()

	firstName := "Elon"
	go fullName(&firstName, nil)

	time.Sleep(10 * time.Second)

	fmt.Println("returned normally from main")
}

这里需要注意,当一个goroutine中发生panic 另一个goroutine如果在panic之前就执行了,就没啥,负责就会被挂掉。

image.png

example2 runtime panic (out range of slice
package main

import (
	"fmt"
)

func slicePanic() {
	n := []int{5, 7, 4}
	fmt.Println(n[4])
	fmt.Println("normally returned from a")
}
func main() {
	slicePanic()
	fmt.Println("normally returned from main")
}
panic: runtime error: index out of range [4] with length 3

goroutine 1 [running]:
main.slicePanic()
	/tmp/sandbox942516049/prog.go:9 +0x1d
main.main()
	/tmp/sandbox942516049/prog.go:13 +0x22

Defer Calls During a Panic

When a function encounters a panic, its execution is stopped, any deferred functions are executed and then the control returns to its caller. This process continues until all the functions of the current goroutine have returned at which point the program prints the panic message, followed by the stack trace and then terminates.

package main

import (
	"fmt"
)

func fullName(firstName *string, lastName *string) {
	defer fmt.Println("deferred call in fullName")
	if firstName == nil {
		panic("runtime error: first name cannot be nil")
	}
	if lastName == nil {
		panic("runtime error: last name cannot be nil")
	}
	fmt.Printf("%s %s\n", *firstName, *lastName)
	fmt.Println("returned normally from fullName")
}

func main() {
	defer fmt.Println("deferred call in main")
	firstName := "Elon"
	fullName(&firstName, nil)
	fmt.Println("returned normally from main")
}

deferred call in fullName
deferred call in main
panic: runtime error: last name cannot be nil

goroutine 1 [running]:
main.fullName(0xc00006af28, 0x0)
	/tmp/sandbox451943841/prog.go:13 +0x23f
main.main()
	/tmp/sandbox451943841/prog.go:22 +0xc6

recover

func panic(interface{})
func recover() interface{}

Recover is useful only when called inside deferred functions.

If recover is called outside the deferred function, it will not stop a panicking sequence.

package main

import (  
    "fmt"
)

func recoverFullName() {  
    if r := recover(); r!= nil {
        fmt.Println("recovered from ", r)
    }
}

func fullName(firstName *string, lastName *string) {  
    defer recoverFullName()
    if firstName == nil {
        panic("runtime error: first name cannot be nil")
    }
    if lastName == nil {
        panic("runtime error: last name cannot be nil")
    }
    fmt.Printf("%s %s\n", *firstName, *lastName)
    fmt.Println("returned normally from fullName")
}

func main() {  
    defer fmt.Println("deferred call in main")
    firstName := "Elon"
    fullName(&firstName, nil)
    fmt.Println("returned normally from main")
}

recovered from  runtime error: last name cannot be nil
returned normally from main
deferred call in main
package main

import (
	"fmt"
)

func recoverInvalidAccess() {
	if r := recover(); r != nil {
		fmt.Println("Recovered", r)
	}
}

func invalidSliceAccess() {
	defer recoverInvalidAccess()
	n := []int{5, 7, 4}
	fmt.Println(n[4])
	fmt.Println("normally returned from a")
}

func main() {
	invalidSliceAccess()
	fmt.Println("normally returned from main")
}

Recovered runtime error: index out of range [4] with length 3
normally returned from main

Getting Stack Trace after Recover

If we recover from a panic, we lose the stack trace about the panic. Even in the program above after recovery, we lost the stack trace.

There is a way to print the stack trace using the PrintStack function of the Debug package

package main

import (
	"fmt"
	"runtime/debug"
)

func recoverFullName() {
	if r := recover(); r != nil {
		fmt.Println("recovered from ", r)
		debug.PrintStack()
	}
}

func fullName(firstName *string, lastName *string) {
	defer recoverFullName()
	if firstName == nil {
		panic("runtime error: first name cannot be nil")
	}
	if lastName == nil {
		panic("runtime error: last name cannot be nil")
	}
	fmt.Printf("%s %s\n", *firstName, *lastName)
	fmt.Println("returned normally from fullName")
}

func main() {
	defer fmt.Println("deferred call in main")
	firstName := "Elon"
	fullName(&firstName, nil)
	fmt.Println("returned normally from main")
}
recovered from  runtime error: last name cannot be nil
goroutine 1 [running]:
runtime/debug.Stack(0x37, 0x0, 0x0)
	/usr/local/go-faketime/src/runtime/debug/stack.go:24 +0x9d
runtime/debug.PrintStack()
	/usr/local/go-faketime/src/runtime/debug/stack.go:16 +0x22
main.recoverFullName()
	/tmp/sandbox771195810/prog.go:11 +0xb4
panic(0x4a1b60, 0x4dc300)
	/usr/local/go-faketime/src/runtime/panic.go:969 +0x166
main.fullName(0xc0000a2f28, 0x0)
	/tmp/sandbox771195810/prog.go:21 +0x1cb
main.main()
	/tmp/sandbox771195810/prog.go:30 +0xc6
returned normally from main
deferred call in main

Panic, Recover and Goroutines

Recover works only when it is called from the same goroutine which is panicking. It’s not possible to recover from a panic that has happened in a different goroutine

package main

import (
	"fmt"
)

func recovery() {
	if r := recover(); r != nil {
		fmt.Println("recovered:", r)
	}
}

func sum(a int, b int) {
	defer recovery()
	fmt.Printf("%d + %d = %d\n", a, b, a+b)
	done := make(chan bool)
	go divide(a, b, done)
	<-done
}

func divide(a int, b int, done chan bool) {
	fmt.Printf("%d / %d = %d", a, b, a/b)
	done <- true

}

func main() {
	sum(5, 0)
	fmt.Println("normally returned from main")
}

5 + 0 = 5
panic: runtime error: integer divide by zero

goroutine 18 [running]:
main.divide(0x5, 0x0, 0xc0000a2000)
	/tmp/sandbox877118715/prog.go:22 +0x167
created by main.sum
	/tmp/sandbox877118715/prog.go:17 +0x1a9

If the divide() function is called in the same goroutine, we would have recovered from the panic.

image.png