异常处理
1.异常
在Go语言中,异常被定义为实现了 error 接口的类型;error 接口只包含一个方法 Error() ,用于返回错误信息。
error 除了输出错误外,往往需要输出当时的业务相关信息(错误地址,错误码,错误信息等),举个简单例子:
package main
import (
"fmt"
)
const (
Success = iota
InvalidUser
WrongPassword
)
var code2msg = map[int]string{
Success: "登录成功",
InvalidUser: "无效的用户",
WrongPassword: "密码错误",
}
type LoginError struct { //自定义错误类型,添加想要输出的错误信息
code int
Message string
}
func NewLoginError(code int) *LoginError {
return &LoginError{
code,
code2msg[code],
}
}
func (e *LoginError) Error() string {
return fmt.Sprintf("ERROR DETAIL: error code:[%+v] error message:[%+v]", e.code, e.Message)
}
func (e *LoginError) Code() int {
return e.code
}
func login(i int) (bool, error) {if i != 0 {
return false, NewLoginError(i)
}
return true, nil
}
func testlogin(i int) {
_, err := login(i) //err是error接口类型变量
if err != nil {
//用switch对不同种类错误进行处理
switch err.(type) {
case *LoginError:
loginerror := err.(*LoginError) //类型断言
fmt.Println(loginerror.Error())
//用switch对错误的不同方面进行处理
switch loginerror.Code() {
case InvalidUser:
//...
case WrongPassword:
//...
}
//其他错误
default:
fmt.Println("other error")
}
}
//登录成功操作
}
func main() {
testlogin(1)
}
2.异常的抛出和捕获
panic() 函数
在Go语言中,异常可以通过调用 panic() 函数来抛出。
关于panic:
- 是内置函数
- 假如函数F中 panic,会终止其后要执行的代码,如果函数F存在要执行的 defer 函数列表,按照defer的逆序执行;如果函数F的 defer 函数列表中没有 recover 语句,异常将继续向上抛出,直到被捕获或者程序崩溃。
- defer 语句必须放在 panic 前定义!
recover() 函数
在Go语言中,异常可以通过调用 recover() 函数来捕获。
关于recover:
- 是内置函数
- recover 只有在 defer 调用的函数中才有效。否则当 panic 时,recover 甚至都不会执行。
- recover 可以放在最外层函数,做统一异常处理。
例子
package main
import (
"fmt"
)
type PanicError struct {
msg string
}
func (e *PanicError) Error() string {
return e.msg
}
func test2() {
err := PanicError{"panic error!"} //异常信息
panic(err.Error())
defer func() { //无效的 defer
println("test2_defer")
}()
}
func test1() {
recover() //捕获不到异常
test2()
recover() //捕获不到异常
fmt.Println("test1") //被终止了
}
func test() {
defer func() {
println("test_defer")
if err := recover(); err != nil { //捕获异常,上层函数得以正常执行
println(err.(string)) //最外层函数对异常处理
}
}()
test1()
fmt.Println("test") //被终止了
}
func A() {
test()
fmt.Println("A")
}
func main() {
fmt.Println("START")
A()
fmt.Println("END")
}
//输出结果:
//START
//A
//END
//test_defer
//panic error!
3.进一步了解
defer
defer的实现过程:
1.运行到 defer 语句时,生成对应的 _defer 结构体实例,存到栈中,再调用 deferprocStack( *_defer ) 函数,将其加入当前 g 的 defer 链表头
(1).如果 defer 语句在循环中,就调用 deferproc( fn func() ) 函数,在里面生成对应的 _defer 结构体实例存到堆中,再将其加入当前 goroutine 的 defer 链表头
- return 语句给返回值复制后,调用 deferreturn() 函数,不断获取链表头的 _defer 结构体执行,直到链表为空 或 取得的 _defer 结构体不是当前函数调用的
3.return 语句执行 RET ,返回上层函数。
_defer 结构体数据结构如下:
源码文件:src/runtime/runtime2.go line:1026
type _defer struct {
heap bool //是否存在堆上
rangefunc bool
sp uintptr //调用者栈指针
pc uintptr //返回地址
fn func() //函数
link *_defer //链表中下一个defer
head *atomic.Pointer[_defer]
}
开放编码优化(open-coded defer)
实现 open-coded defer 需要满足三个条件:
- 没有禁用编译器优化
- 函数的 defer 关键字不能在循环中执行
- 函数的 defer 数量少于或者等于 8 个,函数的 return 个数与 defer 函数个数的乘积小于或者等于 15 个
查看是否 open-coded defer 可以在终端输入:go build -gcflags="-d defer" main.go
panic
数据结构:
源码文件:src/runtime/runtime2.go line:1047
type _panic struct {
argp unsafe.Pointer // 指向 defer 调用时参数的指针
arg any // 调用 panic 时传入的参数
link *_panic // 指向前一个 _panic
startPC uintptr
startSP unsafe.Pointer
//指向当前运行的defer的栈帧
sp unsafe.Pointer
lr uintptr
fp unsafe.Pointer
retpc uintptr
//用于处理 open-code-defer 的额外状态
deferBitsPtr *uint8
slotsPtr unsafe.Pointer
recovered bool //当前 _panic 是否被恢复
goexit bool
deferreturn bool
}
执行过程:
1.调用 gopanic,以下操作都在这个函数中
2.创建新的 _panic 并添加到所在 goroutine 的 _panic 链表的最前面
3.不断从当前 goroutine 的 _defer 中链表获取 _defer ,运行延迟调用函数
4.调用 fatalpanic 中止整个程序
recover
recover() 函数非常简单:
- 取出当前 goroutine 的 _panic 链表最新的一个 _panic,将其 recoverd 字段赋值 true
- 若链表为空,则返回 nil
- 完成
4.一些小思考
什么情况下会发生 _panic 链表有多个 _panic?
答:那就是在处理 panic 时,又调用了 panic,这只能在 defer 函数上才能做到。
多个 _panic 该怎么处理?
答:就像函数嵌套一样,从外层到内层处理