panic 宕机
介绍
- 宕机不是一件很好的事情,可能造成体验停止、服务中断,就像没有人希望在取钱时遇到 ATM 机蓝屏一样,但是,如果在损失发生时,程序没有因为宕机而停止,那么用户将会付出更大的代价,这种代价可以是金钱、时间甚至生命,因此,宕机有时也是一种合理的止损方法。
- Go语言的类型系统会在编译时捕获很多错误,但有些错误只能在运行时检查,如数组访问越界、空指针引用等,这些运行时错误会引起宕机。
- 一般而言,当宕机发生时,程序会中断运行,并立即执行在该 goroutine 中被延迟的函数(defer 机制),随后,程序崩溃并输出日志信息,日志信息包括 panic value 和函数调用的堆栈跟踪信息,panic value 通常是某种错误信息。
- 对于每个 goroutine,日志信息中都会有与之相对的发生 panic 时的函数调用堆栈跟踪信息,通常,我们不需要再次运行程序去定位问题,日志信息已经提供了足够的诊断依据,因此,在我们填写问题报告时,一般会将宕机和日志信息一并记录。
- 虽然Go语言的 panic 机制类似于其他语言的异常,但 panic 的适用场景有一些不同,由于 panic 会引起程序的崩溃,因此 panic 一般用于严重错误,如程序内部的逻辑不一致。任何崩溃都表明了我们的代码中可能存在漏洞,所以对于大部分漏洞,我们应该使用Go语言提供的 error 错误机制,而不是 panic。
定义
Go语言使用内建的函数 panic()
就可以造成崩溃
func panic(v interface{}) //panic() 的参数可以是任意类型的。
手动触发
- Go语言可以在程序中手动触发宕机,让程序崩溃。
- 程序在宕机时,会将堆栈和 goroutine 信息进行标准输出,所以宕机也可以方便地知晓发生错误的位置。
代码运行崩溃并输出如下: ```go panic: crashpackage main
func main() {
panic("crash")
}
goroutine 1 [running]: main.main() D:/code/main.go:4 +0x40 exit status 2
---
<a name="gpeVa"></a>
###
<a name="C57YF"></a>
### 方式对比
regexp 是Go语言的正则表达式包,正则表达式需要成功编译后才能使用。这里使用 error 与 panic 对比:
<a name="VyzQi"></a>
##### error
编译正则表达式,当发生错误时:返回 error 同时 Regexp 返回为 nil,该函数适用于在编译错误时获得 error 进行处理,且继续执行后面的代码。
```go
func Compile(expr string) (*Regexp, error) {
return compile(expr, syntax.Perl, false)
}
panic
编译正则表达式,当发生错误时:使用 panic 触发宕机,该函数适用于直接使用正则表达式而无须处理正则表达式错误的情况。
func MustCompile(str string) *Regexp {
regexp, error := Compile(str)
if error != nil {
panic(`regexp: Compile(` + quote(str) + `): ` + error.Error())
}
return regexp
}
手动宕机进行报错的方式不是一种偷懒的方式,反而能迅速报错,终止程序继续运行,防止更大的错误产生,不过,如果任何错误都使用宕机处理,也不是一种良好的设计习惯,因此应根据需要来决定是否使用宕机进行报错。
这里推荐使用 error 来解决该问题。
延迟执行
当 panic() 触发宕机时,panic() 后面的代码将不会被运行,但是在之前已经运行过的 defer 语句依然会在宕机发生时发生作用。这个特性可以用来在宕机发生前进行宕机信息处理。
func main() {
defer fmt.Println("宕机后要做的事情1")
defer fmt.Println("宕机后要做的事情2")
panic("宕机")
fmt.Println("宕机后要做的事情3")
}
运行结果:
宕机后要做的事情2
宕机后要做的事情1
panic: 宕机
goroutine 1 [running]:
main.main()
D:/code/main.go:4 +0xf8
exit status 2
recover 恢复
介绍
- 在其他语言里,宕机往往以异常的形式存在,底层抛出异常,上层逻辑通过 try/catch 机制捕获异常,没有被捕获的严重异常会导致宕机,捕获的异常可以被忽略,让代码继续运行。
- Go语言没有异常系统,其使用 panic 触发宕机类似于其他语言的抛出异常,recover 的宕机恢复机制就对应其他语言中的 try/catch 机制。
- Recover 是一个Go语言的内建函数,可以让进入宕机流程中的 goroutine 恢复过来,recover 仅在延迟函数 defer 中有效,在正常的执行过程中,调用 recover 会返回 nil 并且没有其他任何效果,如果当前的 goroutine 陷入恐慌,调用 recover 可以捕获到 panic 的输入值,并且恢复正常的执行。
- 通常来说,不应该对进入 panic 宕机的程序做任何处理,但有时,需要我们可以从宕机中恢复,至少我们可以在程序崩溃前,做一些操作,举个例子,当 web 服务器遇到不可预料的严重问题时,在崩溃前应该将所有的连接关闭,如果不做任何处理,会使得客户端一直处于等待状态,如果 web 服务器还在开发阶段,服务器甚至可以将异常信息反馈到客户端,帮助调试。
使用
defer func() {
if r := recover(); r != nil {
fmt.Println(r)
}
}()
继续执行
当传入函数以任何形式发生 panic 崩溃后,可以将崩溃发生的错误打印出来,同时允许后面的代码继续运行,不会造成整个进程的崩溃。
package main
import (
"fmt"
"runtime"
)
// 崩溃时需要传递的上下文信息
type panicContext struct {
function string // 所在函数
}
// 保护方式允许一个函数
func ProtectRun(entry func()) {
// 延迟处理的函数
defer func() {
// 发生宕机时,获取panic传递的上下文并打印
err := recover()
switch err.(type) {
case runtime.Error: // 运行时错误
fmt.Println("runtime error:", err)
default: // 非运行时错误
fmt.Println("error:", err)
}
}()
entry()
}
func main() {
fmt.Println("运行前")
// 允许一段手动触发的错误
ProtectRun(func() {
fmt.Println("手动宕机前")
// 使用panic传递上下文
panic(&panicContext{
"手动触发panic",
})
fmt.Println("手动宕机后")
})
// 故意造成空指针访问错误
ProtectRun(func() {
fmt.Println("赋值宕机前")
var a *int
*a = 1
fmt.Println("赋值宕机后")
})
fmt.Println("运行后")
}
输出结果:
运行前
手动宕机前
error: &{手动触发panic}
赋值宕机前
runtime error: runtime error: invalid memory address or nil pointer dereference
运行后
panic 和 recover 的关系
关系
- 虽然 panic/recover 能模拟其他语言的异常机制,但不建议在编写普通函数时也经常性使用这种特性。更推荐使用 error 来解决错误。
- 在 panic 触发的 defer 函数内,可以继续调用 panic,进一步将错误外抛,直到程序整体崩溃。
- 如果想在捕获错误时设置当前函数的返回值,可以对返回值使用命名返回值方式直接进行设置。