1、宕机
Go语言的类型系统会在编译时捕获很多错误,但有些错误只能在运行时检查,如数组访问越界、空指针引用等,这些运行时错误会引起宕机。
一般而言,当宕机发生时,程序会中断运行,并立即执行在该goroutine(线程)中被延迟的函数(defer机制),随后,程序崩溃并输出日志信息。日志信息包括panic value和函数调用的堆栈跟踪信息,panicvalue通常是某种错误信息。
引发宕机有如下两种情况:
- 程序主动调用panic()函数。
- 程序产生运行时错误,由运行时检测并抛出。
发生panic后,程序会从调用panic的函数位置或发生panic的地方立即返回,逐层向上执行函数的defer语句,然后逐层打印函数调用堆栈,直到被recover捕获或运行到最外层函数而退出。
panic的参数是一个空接口类型interface{},所以,任意类型的变量都可以传递给panic。调用panic的方法非常简单,即panic (xxx)。
panic不但可以在函数正常流程中抛出,在defer逻辑中也可以再次调用panic或抛出panic。defer中的panic能够被后续执行的defer捕获。
Go语言可以在程序中手动触发宕机,让程序崩溃,这样开发者可以及时发现错误,同时减少可能的损失。
Go语言程序在宕机时,会将堆栈和goroutine信息输出到控制台,所以,宕机也可以方便地确定发生错误的位置。
func main() {
panic("crash")
}
当panic()触发的宕机发生时,panic()后面的代码将不会被运行,但是在panic()函数前面已经运行过的defer语句依然会在宕机发生时发生作用:
func main() {
defer fmt.Println("宕机时要做的事情1...")
defer fmt.Println("宕机时要做的事情2...")
panic("crash")
}
2、宕机恢复
无论代码运行错误是由Runtime层抛出的panic崩溃,还是主动触发的panic崩溃,都可以配合defer和recover实现错误的捕捉和恢复,让代码发生崩溃后允许继续运行。
recover()用来捕获panic,阻止panic继续向上传递。recover()函数可以和defer语句一起使用,但recover()函数只有在defer后面的函数体内被直接调用才能捕获panic终止异常,否则会返回nil,异常继续向外传递。
可以有连续多个panic被抛出,连续多个被抛出的场景只能出现在延迟调用中。虽然有多个panic被抛出,但是只有最后一次的panic才能被捕获:
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
}()
// 只有最后一次的panic调用能够被捕获
defer func() {
panic("first defer panic")
}()
defer func() {
panic("second defer panic")
}()
panic("main body panic")
}
panic和recover的关系如下:
- 有panic没recover,程序宕机。
- 有panic也有recover,程序不会宕机,执行完对应的defer语句后,从宕机点退出当前函数后继续执行。
3、错误与处理
Go语言的错误处理过程如下:
- 一个可能造成错误的函数,需要返回值中返回一个错误接口,如果调用是成功的,错误接口将返回nil,否则返回错误。
- 在函数调用后需要检查错误,如果发生错误,则进行必要的错误处理。
a. 错误接口
error是Go语言系统声明的接口类型,语法格式:type error interface {Error() string}
所有符合Error()string格式的方法,都能实现错误接口,Error()方法返回错误的具体描述,使用者可以通过该字符串知道发生了什么错误。
Go语言内置错误接口类型error。任何类型只要实现Error() string方法,都可以传递error接口类型变量。Go语言典型的错误处理方式是将error作为函数最后一个返回值。在调用函数时,通过检测其返回的error值是否为nil来进行错误处理。
b. 自定义错误
返回错误前,需要定义会产生哪些可能的错误,在Go语言中,使用errors包进行错误的定义,格式如下:
var err = errors.New("this is an error")
错误字符串由于相对固定,一般在包作用域声明,应尽量减少在使用时直接使用errors.New返回。
errors 包:Go语言的errors中对New的定义非常简单
// 创建错误对象
func New(text string) error {
return &errorString{text}
}
// 错误字符串
type errorString struct {
s string
}
// 返回发生何种错误
func (e *errorString) Error() string {
return e.s
}
- 第2行:将errorString结构体实例化,并赋值错误描述的成员。
- 第7行:声明errorString结构体,拥有一个成员,描述错误内容。
- 第12行:实现error接口的Error()方法,该方法返回成员中的错误描述。
在代码中使用错误定义——例如:定义一个除法函数,当除数为0时,返回一个预定义的除数为0的错误。
// 定义除数为0的错误
var errDivisionByZero = errors.New("division by zero")
func div(dividend, divisor int) (int, error) {
// 判断除数为0的情况并返回
if divisor == 0 {
return 0, errDivisionByZero
}
// 正常计算,返回空错误
return dividend / divisor, nil
}
func main() {
fmt.Println(div(1, 0))
}
c. 错误和异常
错误是指发生非期望的已知行为,这里的已知是指错误的类型是预料并定义好的。
异常是指发生非期待的未知行为。这里的未知是指错误的类型不在预先定义的范围内。异常又被称为未捕获的错误(untrapped error)。程序在执行时发生未预先定义的错误,程序编译器和运行时都没有及时将其捕获处理,而是由操作系统进行异常处理。
在Go语言中对于错误提供了两种处理机制:
- 通过函数返回错误类型的值来处理错误。
- 通过panic打印程序调用栈,终止程序执行来处理错误。
对错误的处理也有两种方法:
- 一种是通过返回一个错误类型值来处理错误,
- 另一种是直接调用panic抛出错误,退出程序。
Go是静态强类型语言,程序的大部分错误是可以在编译器检测到的,但是有些错误行为需要在运行期才能检测出来。此种错误行为将导致程序异常退出。其表现出的行为就和直接调用panic一样:打印出函数调用栈信息,并且终止程序执行。
在实际的编程中,error和panic的使用应该遵循以下原则:
(1)程序发生的错误导致程序不能容错继续执行,此时程序应该主动调用panic或由运行时抛出panic。
(2)程序虽然发生错误,但是程序能够容错继续执行,此时应该使用错误返回值的方式处理错误,或者在可能发生运行时错误的非关键分支上使用recover捕获panic。