| by suyi | No comments

Go:Panic与Recover

1.Panic例子

package main

import (  
    "fmt"
)

func fullName(firstName *string, lastName *string) {  
    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() {  
    firstName := "SS"
    fullName(&firstName, nil)
    fmt.Println("returned normally from main")
}

运行输出:

panic: runtime error: last name cannot be nil

goroutine 1 [running]:  
main.fullName(0xc00006af58, 0x0)  
    /tmp/sandbox210590465/prog.go:12 +0x193
main.main()  
    /tmp/sandbox210590465/prog.go:20 +0x4d

从例子中可以看到,当给firstName分配值并未给lastName分配,调用fullName,当执行到 lastName == nil 时,程序会被终止,传递给panic函数的参数被打印出来,然后是堆栈信息 。由于程序在执行 panic 函数调用之后终止,后面代码将不再执行。

所以该程序首先打印传递给panic函数的消息

panic: runtime error: last name cannot be nil

然后打印堆栈信息,根据堆栈跟踪,一项一项的打印。

2.例2

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")
}

例子中试图访问切片中n[4]的无效索引,则该程序将有以下输出:

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

3.在Panic中使用defer

当一个函数遇到panic时,它的执行会停止,但是任何defer的函数都会被执行,然后控制权返回给它的调用者。这个过程一直持续到当前 goroutine 的所有函数都已经返回,此时程序会打印panic消息,然后是堆栈跟踪,然后终止。

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 := "SS"
    fullName(&firstName, nil)
    fmt.Println("returned normally from main")
}

上例唯一更改是添加了defer函数调用,现在的输出为:

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

当程序遇到panic时,首先执行defer的函数调用,然后控制返回到执行defer调用的调用者,依此类推。 最后是堆栈跟踪信息。

4.panic中使用recover

仅当在defer函数内部调用时,recover才有用。在defer函数内执行recover恢复正常执行来停止panic,并检索传递给panic函数调用的错误消息。如果在defer函数之外调用recover,并不会停止panic。

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 := "SS"
    fullName(&firstName, nil)
    fmt.Println("returned normally from main")
}

调用recover()返回传递给panic函数调用的值, 当fullName函数调用时,recoverFullName()将调用defer函数,用于recover()停止panic,打印结果

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

当程序在 lastName == nil 发生panic时。recoverFullName函数被调用,该函数又调用recover()以重新获得对panic的控制。并将返回值传递给的panic(),因此它打印:

recovered from  runtime error: last name cannot be nil  

执行后recover(),panic停止,控制权返回给调用者,在这种情况下,是main函数。程序从 fmt.Println(“returned normally from main”) 开始继续正常执行,main的panic已经recover了。它打印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  

5.recover后获取stack跟踪

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 := "SS"
    fullName(&firstName, nil)
    fmt.Println("returned normally from main")
}

在上面的例子中,使用debug.PrintStack()打印堆栈信息。

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被恢复并recovered from runtime error: last name cannot be nil打印出来了之后,打印堆栈跟踪。然后在panic恢复后打印

returned normally from main  
deferred call in main 

6.panic、recover和Goroutines

Recover 仅在从恐慌的同一个 goroutine 中调用时才起作用。无法从不同 goroutine 中发生的panic中恢复过来。

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")
}

在上面的例子中,该函数divide()将发生panic。因为 b 是零并且零不能做除数。该sum()函数调用一个defer函数recovery(),用于从panic中恢复。该函数divide()作为单独的 goroutine 调用,在done channel等待,确保divide()完成并执行。

该例中panic将无法recover。这是因为该recovery函数存在于不同的 goroutine 中,而panic发生在divide()不同 goroutine中的函数中。因此recover是不可能的。

这个例子打印如下:

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

可以从输出中看出panic并未recover。 如果divide()在同一个 goroutine 中调用该函数,就会从panic中recover过来

如果将上面例子中 go divide(a, b, done) 改为 divide(a, b, done) ,panic将会被recover,因为panic发生在同一个 goroutine 中,则它将打印:

5 + 0 = 5  
recovered: runtime error: integer divide by zero  
normally returned from main  

发表评论