Golang错误处理
错误处理
异常处理
Go的设计者认为其它语言异常处理太过消耗资源,且设计和处理复杂,导致使用者不能很好的处理错误,甚至觉得异常和错误处理起来麻烦而忽视、忽略掉,从而导致程序崩溃。
- 函数调用,返回值可以返回多值,一般最后一个值可以是error接口类型的值
- 如果函数调用产生错误,则这个值是一个error接口类型的错误
- 如果函数调用成功,则该值是nil
- 检查函数返回值中的错误是否是nil,如果不是nil,进行必要的错误处理
error是Go中声明的接口类型
type error interface{
Error() string
}
所有实现Error() string
签名的方法,都可以实现错误接口。用Error()方法返回错误的具体描述
自定义error
package main
import "fmt"
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s }
func New(text string) error {
return &errorString{text} // var e error = &errorString{text}
}
func main() {
var e = errorString{"错误理由1"} // 请问和14行有区别吗?
fmt.Println(e, e.Error(), &e, (&e).Error())
var err = New("错误理由2") // 请问e和err有什么区别?
fmt.Println(err.Error())
}
上例第12行是直接通过结构体实例化一个实例,而14行是通过构造方法返回一个结构体实例的指针,而errorString结构体又实现了error接口(实现了Error方法),所以该结构体的类型既是errorString类型,也是error类型
通过errors包的New方法返回一个error接口类型的错误实例,errors.New("错误描述")
来创建一个新的错误
可以看出New方法返回一个实现了Error接口的结构体实例的指针
package main
import (
"errors"
"fmt"
)
var ErrDivisionByZero = errors.New("division by zero") // 构造一个错误实例,建议
Err前缀
func div(a, b int) (int, error) {
if b == 0 {
return 0, ErrDivisionByZero
}
return a / b, nil
}
func main() {
if r, err := div(5, 0); err != nil {
// fmt.Println(err)
fmt.Println(err.Error())
} else {
fmt.Println(r)
}
}
Panic
panic有人翻译成宕机。
panic是不好的,因为它发生时,往往会造成程序崩溃、服务终止等后果,所以没人希望它发生。但是,如果在错误发生时,不及时panic而终止程序运行,继续运行程序恐怕造成更大的损失,付出更加惨痛的代价。所以,有时候,panic导致的程序崩溃实际上可以及时止损,只能两害相权取其轻。
panic虽然不好,体验很差,但也是万不得已,可以马上暴露问题,及时发现和纠正问题。
panic产生
- runtime运行时错误导致抛出panic,比如数组越界、除零
- 主动手动调用panic(reason),这个reason可以是任意类型
panic执行
- 逆序执行当前已经注册过的goroutine的defer链(recover从这里介入)
- 打印错误信息和调用堆栈
- 调用exit(2)结束整个进程
package main
import (
"fmt"
)
func div(a, b int) int {
defer fmt.Println("start")
defer fmt.Println(a, b)
r := a / b // 这一行有可能panic
fmt.Println("end")
return r }
func main() {
fmt.Println(div(5, 0))
}
运行后程序崩溃,因为除零异常,输入如下
5 0
start
panic: runtime error: integer divide by zero
goroutine 1 [running]:下面是调用栈,div压着main
main.div(0x5, 0x0)
O:/pros/main.go:13 +0x1bf 出错的行号
main.main()
O:/pros/main.go:19 +0x25
exit status 2
recover
recover即恢复,defer和recover结合起来,在defer中调用recover来实现对错误的捕获和恢复,让代码在发生panic后通过处理能够继续运行。类似其它语言中try/catch。
err := recover()
,v := err.(type)
,v就是panic(reason)
中的reason,reason可以是任意类型。
package main
import (
"errors"
"fmt"
"runtime"
)
var ErrDivisionByZero = errors.New("除零异常")
func div(a, b int) int {
defer func() {
err := recover()
fmt.Printf("1 %+v, %[1]T\n", err)
}()
defer fmt.Println("start")
defer fmt.Println(a, b)
defer func() {
err := recover() // 一旦recover了,就相当处理过了错误
fmt.Printf("2 %+v, %[1]T\n", err)
switch v := err.(type) { // 类型断言
case runtime.Error:
// 在源码runtime/error.go中75行,还为errorString也实现了RuntimeError方法
// 也就是说,标准库runtime运行时错误,都是runtime.Error接口的
fmt.Printf("原因:%T, %#[1]v\n", v)
case []int:
fmt.Println("原因:切片")
}
fmt.Println("离开recover处理")
}()
r := a / b
panic([]int{1, 3, 5}) // 手动panic
fmt.Println("end")
return r }
func main() {
fmt.Println(div(5, 0), "!!!") // 有返回值吗?如果有是什么?
fmt.Println("main exit") }
上例中,一旦在某函数中panic,当前函数panic之后的语句将不再执行,开始执行defer。如果在defer中错误被recover后,就相当于当前函数产生的错误得到了处理。当前函数执行完defer,当前函数退出执行,程序还可以从当前函数之后继续执行。
可以观察到panic和recover有如下
- 有panic,一路向外抛出,但没有一处进行recover,也就是说没有地方处理错误,程序崩溃
- 有painc,有recover来捕获,相当于错误被处理掉了,当前函数defer执行完后,退出当前函数,从当前函数之后继续执行