Go
语言的错误处理思想及设计包含以下特征:
一个可能造成错误的函数,需要返回值中返回一个错误接口( error
),如果调用是成功的,错误接口将返回 nil
,否则返回错误。
在函数调用后需要检查错误,如果发生错误,则进行必要的错误处理。
Go
里没有用经典的 try/except
捕获异常。Go
提供两种错误处理方式
error
类型对象判断错误panic
异常一般而言,当宕机发生时,程序会中断运行,并立即执行在该 goroutine
(可以先理解成线程)中被延迟的函数( defer
机制),随后,程序崩溃并输出日志信息,日志信息包括 panic value
和函数调用的堆栈跟踪信息, panic value
通常是某种错误信息。
虽然 Go
语言的 panic
机制类似于其他语言的异常,但 panic
的适用场景有一些不同,由于 panic
会引起程序的崩溃,因此 panic
一般用于严重错误,如程序内部的逻辑不一致。
任何崩溃都表明了我们的代码中可能存在漏洞,所以对于大部分漏洞,我们应该使用 Go
语言提供的错误机制,而不是 panic
。
一般情况下在 Go
里只使用 error
类型判断错误, Go
官方希望开发者能够很清楚的掌控所有的异常,在每一个可能出现异常的地方都返回或判断 error
是否存在。
panic
可以手工调用,但是 Go
官方建议尽量不要使用panic
,每一个异常都应该用 error
对象捕获。
如果异常出现了,但没有被捕获并恢复,Go
程序的执行就会被终止,即便出现异常的位置不在主 Goroutine
中也会这样。
panic
使用 panic
抛出异常后,函数执行将从调用 panic
的地方停止,如果函数内有 defer
调用,则执行 defer
后边的函数调用,如果 defer
调用的函数中没有捕获异常信息,这个异常会沿着函数调用栈往上传递,直到 main
函数仍然没有捕获异常,将会导致程序异常退出。示例代码:
package main
func demo() {
panic("抛出异常")
}
func main() {
demo()
}
package main
import (
"fmt"
)
func main() {
panic("crash")
fmt.Println("end")
}
输出结果:
panic: crash
goroutine 1 [running]:
main.main()
/home/wohu/gocode/src/hello.go:8 +0x39
exit status 2
以上代码中只用了一个内建的函数 panic()
就可以造成崩溃, panic()
的声明如下:
func panic(v interface{}) //panic() 的参数可以是任意类型的。
请谨慎使用panic
函数抛出异常,如果没有捕获异常,将会导致程序异常退出。
panic
延迟执行在 Go
中,panic
主要有两类来源,一类是来自 Go
运行时,另一类则是 Go
开发人员通过 panic
函数主动触发的。
当 panic()
触发的宕机发生时, panic()
后面的代码将不会被运行,但是在 panic()
函数前面已经运行过的 defer
语句依然会在宕机发生时发生作用,参考下面代码:
package main
import (
"fmt"
)
func main() {
defer fmt.Println("defef run")
panic("crash")
fmt.Println("end")
}
输出结果:
defef run
panic: crash
goroutine 1 [running]:
main.main()
/home/wohu/gocode/src/hello.go:10 +0x95
exit status 2
从结果中可以看到,触发 panic
前, defer
语句会被优先执行。
panic()
是一个内建函数,可以中断原有的控制流程,进入一个令人 panic
的流程中。当函数 main
调用 panic
,函数的执行被中断,但是 main
中的延迟函数(必须是在 panic
之前的已加载的 defer
)会正常执行,然后 main
返回到调用它的地方。在调用的地方,main
的行为就像调用了 panic
。这一过程继续向上,直到发生 panic
的 goroutine
中所有调用的函数返回,此时程序退出。
异常可以直接调用 panic
产生。也可以由运行时错误产生,例如访问越界的数组。
recover
是一个 Go
语言的内建函数,可以让进入宕机流程中的 goroutine
恢复过来。
recover
仅在延迟函数 defer
中有效:
在正常的执行过程中,调用 recover
会返回 nil
并且没有其他任何效果;
如果当前的 goroutine
陷入 panic
,调用 recover
可以捕获到 panic
的输入值,并且恢复正常的执行;
注意:
在其他语言里,
panic
往往以异常的形式存在,底层抛出异常,上层逻辑通过try/catch
机制捕获异常,没有被捕获的严重异常会导致宕机,捕获的异常可以被忽略,让代码继续运行。
Go
语言没有异常系统,其使用 panic
触发宕机类似于其他语言的抛出异常, recover
的宕机恢复机制就对应其它语言中的 try/catch
机制。
package main
func test() {
defer func() {
if err := recover(); err != nil { // recover 捕获错误。
println(err.(string)) // 将 interface{} 转型为具体类型。
}
}()
panic("panic error!") // panic 抛出错误
}
func main() {
test()
}
由于 panic
、 recover
参数类型为 interface{}
,因此可抛出任何类型对象。
func panic(v interface{})
func recover() interface{}
延迟调用中引发的错误,可被后续延迟调用捕获,但仅最后一个错误可被捕获。
package main
import "fmt"
func test() {
defer func() {
fmt.Println(recover())
}()
defer func() {
panic("defer panic")
}()
panic("test panic")
}
func main() {
test()
}
输出:
defer panic
捕获函数 recover
只有在延迟调用内直接调用才会终止错误,否则总是返回 nil
。任何未捕获的错误都会沿调用堆栈向外传递。
当没有异常信息抛出时, recover
函数返回值是 nil
。 recover
只有在 defer
调用的函数内部时,才能阻止 panic
抛出的异常信息继续向上传递,如果不是在 defer
调用的函数内部,将会失效。
package main
import "fmt"
func test() {
defer recover() // 无效!
defer fmt.Println(recover()) // 无效!
defer func() {
func() {
println("defer inner")
recover() // 无效!
}()
}()
panic("test panic")
}
func main() {
test()
}
输出
defer inner
<nil>
panic: test panic
使用延迟匿名函数或下面这样都是有效的。
package main
import "fmt"
func except() {
fmt.Println(recover())
}
func test() {
defer except()
panic("test panic")
}
func main() {
test()
}
如果需要保护代码片段,可将代码块重构成匿名函数,如此可确保后续代码被执行。
package main
import "fmt"
func test(x, y int) {
var z int
func() {
defer func() {
err := recover()
fmt.Println(err)
if err != nil {
z = 0
}
}()
z = x / y
return
}()
println("x / y =", z)
}
func main() {
test(10, 0)
}
输出结果:
runtime error: integer divide by zero
x / y = 0
recover
的正确用法:
package main
import (
"errors"
"fmt"
)
func main() {
fmt.Println("Enter function main.")
defer func() {
fmt.Println("Enter defer function.")
// recover函数的正确用法。
if p := recover(); p != nil {
fmt.Printf("panic: %s\n", p)
}
fmt.Println("Exit defer function.")
}()
// recover函数的错误用法。
fmt.Printf("no panic: %v\n", recover())
// 引发panic。
panic(errors.New("something wrong"))
// recover函数的错误用法。
p := recover()
fmt.Printf("panic: %s\n", p)
fmt.Println("Exit function main.")
}
panic
和 recover
的关系如何区别使用 panic
和 error
两种方式?
惯例是:导致关键流程出现不可修复性错误的使用 panic
,其他使用 error
。
panic
和 recover
的组合有如下特性:
panic
没 recover
,程序宕机。panic
也有 recover
,程序不会宕机,执行完对应的 defer
后,从宕机点退出当前函数后继续执行。注意:
虽然 panic/recover
能模拟其他语言的异常机制,但并不建议在编写普通函数时也经常性使用这种特性。
在 panic
触发的 defer
函数内,可以继续调用 panic
,进一步将错误外抛,直到程序整体崩溃。
如果想在捕获错误时设置当前函数的返回值,可以对返回值使用命名返回值方式直接进行设置。
Go
并发编程中,每一个 goroutine
出现 panic
,都会让整个进程退出,如果能够捕获异常,那么出现 panic
的时候,整个服务不会挂掉,只是当前导致 panic
的某个 goroutine
会出现异常,通过捕获异常可以继续执行任务,建议还是在某些有必要的条件和入口处进行异常捕获。
常见抛出异常的情况:数组越界、空指针空对象,类型断言失败等。
package main
import (
"fmt"
"time"
)
// 抛出异常,模拟实际 Panic 的场景
func throwException() {
panic("An exception is thrown! Start Panic")
}
// Go 的 defer + recover 来捕获异常
func catchExceptions() {
defer func() {
if e := recover(); e != nil {
fmt.Printf("Panicing %s\n", e)
}
}()
go func() {
// 做具体的实现任务
fmt.Print("do something \n")
}()
throwException()
fmt.Printf("Catched an exceptions\n")
}
func main() {
fmt.Printf("==== start main =====\n")
// 执行一次
catchExceptions()
num := 1
for {
num++
fmt.Printf("\nstart circle num:%v\n", num)
// 循环执行,如果实际项目中,这个函数是主任务的话,需要一个 for 来循环执行,避免捕获一次 Panic 之后就不再继续执行
catchExceptions()
time.Sleep(3 * time.Second)
if num == 5 {
fmt.Printf("==== end main =====\r\n")
return
}
}
}
一般的建议是在请求来源入口处的函数或者关键路径上实现这么一段代码进行捕获,这样,只要通过这个入口出现的异常都能被捕获,并打印详细日志。同时,为了保证 goroutine
能够继续执行任务,因此还要考虑当出现 panic
被捕获之后,是否有主动循环或者被动触发来重新执行任务。
Go
标准库提供的 http server
采用的是,每个客户端连接都使用一个单独的 Goroutine
进行处理的并发处理模型。也就是说,客户端一旦与 http server
连接成功,http server
就会为这个连接新创建一个 Goroutine
,并在这 Goroutine
中执行对应连接(conn
)的 serve
方法,来处理这条连接上的客户端请求。
无论在哪个 Goroutine
中发生未被恢复的 panic
,整个程序都将崩溃退出。所以,为了保证处理某一个客户端连接的 Goroutine
出现 panic
时,不影响到 http server
主 Goroutine
的运行,Go
标准库在 serve
方法中加入了对 panic
的捕捉与恢复,下面是 serve
方法的部分代码片段:
// $GOROOT/src/net/http/server.go
// Serve a new connection.
func (c *conn) serve(ctx context.Context) {
c.remoteAddr = c.rwc.RemoteAddr().String()
ctx = context.WithValue(ctx, LocalAddrContextKey, c.rwc.LocalAddr())
defer func() {
if err := recover(); err != nil && err != ErrAbortHandler {
const size = 64 << 10
buf := make([]byte, size)
buf = buf[:runtime.Stack(buf, false)]
c.server.logf("http: panic serving %v: %v\n%s", c.remoteAddr, err, buf)
}
if !c.hijacked() {
c.close()
c.setState(c.rwc, StateClosed, runHooks)
}
}()
... ...
}
你可以看到,serve
方法在一开始处就设置了 defer
函数,并在该函数中捕捉并恢复了可能出现的 panic
。这样,即便处理某个客户端连接的 Goroutine
出现 panic
,处理其他连接 Goroutine
以及 http server
自身都不会受到影响。
这种局部不要影响整体的异常处理策略,在很多并发程序中都有应用。并且,捕捉和恢复 panic
的位置通常都在子 Goroutine
的起始处,这样设置可以捕捉到后面代码中可能出现的所有 panic
,就像 serve
方法中那样。
在 json
包的 encode.go
中也有使用 panic
做潜在 bug
提示的例子:
// $GOROOT/src/encoding/json/encode.go
func (w *reflectWithString) resolve() error {
... ...
switch w.k.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
w.ks = strconv.FormatInt(w.k.Int(), 10)
return nil
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
w.ks = strconv.FormatUint(w.k.Uint(), 10)
return nil
}
panic("unexpected map key type")
}
这段代码中,resolve
方法的最后一行代码就相当于一个“代码逻辑不会走到这里”的断言。一旦触发“断言”,这很可能就是一个潜在 bug。
我们也看到,去掉这行代码并不会对 resolve
方法的逻辑造成任何影响,但真正出现问题时,开发人员就缺少了“断言”潜在 bug
提醒的辅助支持了。在 Go
标准库中,大多数 panic
的使用都是充当类似断言的作用的。