Golang学习笔记(七)—— 异常处理
异常处理
异常
在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) }
异常的抛出和捕获
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!
进一步了解
defer
defer的实现过程:
1.运行到 defer 语句时,生成对应的 _defer 结构体实例,存到栈中,再调用 deferprocStack( *_defer ) 函数,将其加入当前 g 的 defer 链表头
(1).如果 defer 语句在循环中,就调用 deferproc( fn func() ) 函数,在里面生成对应的 _defer 结构体实例存到堆中,再将其加入当前 goroutine 的 defer 链表头
2. 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
- 完成
一些小思考
什么情况下会发生 _panic 链表有多个 _panic?
答:那就是在处理 panic 时,又调用了 panic,这只能在 defer 函数上才能做到。
多个 _panic 该怎么处理?
答:就像函数嵌套一样,从外层到内层处理
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 25岁的心里话
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现