Go 学习笔记(19)— 函数(05)[如何触发 panic、触发 panic 延迟执行、panic 和 recover 的关系]
1. 异常设计思想
Go
语言的错误处理思想及设计包含以下特征:
一个可能造成错误的函数,需要返回值中返回一个错误接口(
error
),如果调用是成功的,错误接口将返回nil
,否则返回错误。在函数调用后需要检查错误,如果发生错误,则进行必要的错误处理。
Go
里没有用经典的 try/except
捕获异常。Go
提供两种错误处理方式
- 函数返回
error
类型对象判断错误 panic
异常
一般而言,当宕机发生时,程序会中断运行,并立即执行在该 goroutine
(可以先理解成线程)中被延迟的函数( defer
机制),随后,程序崩溃并输出日志信息,日志信息包括 panic value
和函数调用的堆栈跟踪信息, panic value
通常是某种错误信息。
虽然 Go
语言的 panic
机制类似于其他语言的异常,但 panic
的适用场景有一些不同,由于 panic
会引起程序的崩溃,因此 panic
一般用于严重错误,如程序内部的逻辑不一致。
任何崩溃都表明了我们的代码中可能存在漏洞,所以对于大部分漏洞,我们应该使用 Go
语言提供的错误机制,而不是 panic
。
一般情况下在 Go
里只使用 error
类型判断错误, Go
官方希望开发者能够很清楚的掌控所有的异常,在每一个可能出现异常的地方都返回或判断 error
是否存在。
panic
可以手工调用,但是 Go
官方建议尽量不要使用panic
,每一个异常都应该用 error
对象捕获。
如果异常出现了,但没有被捕获并恢复,Go
程序的执行就会被终止,即便出现异常的位置不在主 Goroutine
中也会这样。
2. 如何触发 panic
使用 panic
抛出异常后,函数执行将从调用 panic
的地方停止,如果函数内有 defer
调用,则执行 defer
后边的函数调用,如果 defer
调用的函数中没有捕获异常信息,这个异常会沿着函数调用栈往上传递,直到 main
函数仍然没有捕获异常,将会导致程序异常退出。示例代码:
package main
func demo() {panic("抛出异常")
}
func main() {demo()
}
package mainimport ("fmt"
)func main() {panic("crash")fmt.Println("end")}
输出结果:
panic: crashgoroutine 1 [running]:
main.main()/home/wohu/gocode/src/hello.go:8 +0x39
exit status 2
以上代码中只用了一个内建的函数 panic()
就可以造成崩溃, panic()
的声明如下:
func panic(v interface{}) //panic() 的参数可以是任意类型的。
请谨慎使用panic
函数抛出异常,如果没有捕获异常,将会导致程序异常退出。
3. 触发 panic
延迟执行
在 Go
中,panic
主要有两类来源,一类是来自 Go
运行时,另一类则是 Go
开发人员通过 panic
函数主动触发的。
当 panic()
触发的宕机发生时, panic()
后面的代码将不会被运行,但是在 panic()
函数前面已经运行过的 defer
语句依然会在宕机发生时发生作用,参考下面代码:
package mainimport ("fmt"
)func main() {defer fmt.Println("defef run")panic("crash")fmt.Println("end")}
输出结果:
defef run
panic: crashgoroutine 1 [running]:
main.main()/home/wohu/gocode/src/hello.go:10 +0x95
exit status 2
从结果中可以看到,触发 panic
前, defer
语句会被优先执行。
panic()
是一个内建函数,可以中断原有的控制流程,进入一个令人 panic
的流程中。当函数 main
调用 panic
,函数的执行被中断,但是 main
中的延迟函数(必须是在 panic
之前的已加载的 defer
)会正常执行,然后 main
返回到调用它的地方。在调用的地方,main
的行为就像调用了 panic
。这一过程继续向上,直到发生 panic
的 goroutine
中所有调用的函数返回,此时程序退出。
异常可以直接调用 panic
产生。也可以由运行时错误产生,例如访问越界的数组。
4. recover 使用
recover
是一个 Go
语言的内建函数,可以让进入宕机流程中的 goroutine
恢复过来。
recover
仅在延迟函数 defer
中有效:
在正常的执行过程中,调用
recover
会返回nil
并且没有其他任何效果;如果当前的
goroutine
陷入panic
,调用recover
可以捕获到panic
的输入值,并且恢复正常的执行;
注意:
在其他语言里,
panic
往往以异常的形式存在,底层抛出异常,上层逻辑通过try/catch
机制捕获异常,没有被捕获的严重异常会导致宕机,捕获的异常可以被忽略,让代码继续运行。
Go
语言没有异常系统,其使用 panic
触发宕机类似于其他语言的抛出异常, recover
的宕机恢复机制就对应其它语言中的 try/catch
机制。
package mainfunc test() {defer func() {if err := recover(); err != nil { // recover 捕获错误。println(err.(string)) // 将 interface{} 转型为具体类型。}}()panic("panic error!") // panic 抛出错误
}
func main() {test()
}
由于 panic
、 recover
参数类型为 interface{}
,因此可抛出任何类型对象。
func panic(v interface{})
func recover() interface{}
延迟调用中引发的错误,可被后续延迟调用捕获,但仅最后一个错误可被捕获。
package mainimport "fmt"func test() {defer func() {fmt.Println(recover())}()defer func() {panic("defer panic")}()panic("test panic")
}
func main() {test()
}
输出:
defer panic
捕获函数 recover
只有在延迟调用内直接调用才会终止错误,否则总是返回 nil
。任何未捕获的错误都会沿调用堆栈向外传递。
当没有异常信息抛出时, recover
函数返回值是 nil
。 recover
只有在 defer
调用的函数内部时,才能阻止 panic
抛出的异常信息继续向上传递,如果不是在 defer
调用的函数内部,将会失效。
package mainimport "fmt"func test() {defer recover() // 无效!defer fmt.Println(recover()) // 无效!defer func() {func() {println("defer inner")recover() // 无效!}()}()panic("test panic")
}
func main() {test()
}
输出
defer inner
<nil>
panic: test panic
使用延迟匿名函数或下面这样都是有效的。
package mainimport "fmt"func except() {fmt.Println(recover())
}
func test() {defer except()panic("test panic")
}
func main() {test()
}
如果需要保护代码片段,可将代码块重构成匿名函数,如此可确保后续代码被执行。
package mainimport "fmt"func test(x, y int) {var z intfunc() {defer func() {err := recover()fmt.Println(err)if err != nil {z = 0}}()z = x / yreturn}()println("x / y =", z)
}
func main() {test(10, 0)
}
输出结果:
runtime error: integer divide by zero
x / y = 0
recover
的正确用法:
package mainimport ("errors""fmt"
)func main() {fmt.Println("Enter function main.")defer func() {fmt.Println("Enter defer function.")// recover函数的正确用法。if p := recover(); p != nil {fmt.Printf("panic: %s\n", p)}fmt.Println("Exit defer function.")}()// recover函数的错误用法。fmt.Printf("no panic: %v\n", recover())// 引发panic。panic(errors.New("something wrong"))// recover函数的错误用法。p := recover()fmt.Printf("panic: %s\n", p)fmt.Println("Exit function main.")
}
5. panic
和 recover
的关系
如何区别使用 panic
和 error
两种方式?
惯例是:导致关键流程出现不可修复性错误的使用 panic
,其他使用 error
。
panic
和 recover
的组合有如下特性:
- 有
panic
没recover
,程序宕机。 - 有
panic
也有recover
,程序不会宕机,执行完对应的defer
后,从宕机点退出当前函数后继续执行。
注意:
虽然 panic/recover
能模拟其他语言的异常机制,但并不建议在编写普通函数时也经常性使用这种特性。
在 panic
触发的 defer
函数内,可以继续调用 panic
,进一步将错误外抛,直到程序整体崩溃。
如果想在捕获错误时设置当前函数的返回值,可以对返回值使用命名返回值方式直接进行设置。
6. 实际项目使用
Go
并发编程中,每一个 goroutine
出现 panic
,都会让整个进程退出,如果能够捕获异常,那么出现 panic
的时候,整个服务不会挂掉,只是当前导致 panic
的某个 goroutine
会出现异常,通过捕获异常可以继续执行任务,建议还是在某些有必要的条件和入口处进行异常捕获。
常见抛出异常的情况:数组越界、空指针空对象,类型断言失败等。
package mainimport ("fmt""time"
)// 抛出异常,模拟实际 Panic 的场景
func throwException() {panic("An exception is thrown! Start Panic")
}// Go 的 defer + recover 来捕获异常
func catchExceptions() {defer func() {if e := recover(); e != nil {fmt.Printf("Panicing %s\n", e)}}()go func() {// 做具体的实现任务fmt.Print("do something \n")}()throwException()fmt.Printf("Catched an exceptions\n")
}func main() {fmt.Printf("==== start main =====\n")// 执行一次catchExceptions()num := 1for {num++fmt.Printf("\nstart circle num:%v\n", num)// 循环执行,如果实际项目中,这个函数是主任务的话,需要一个 for 来循环执行,避免捕获一次 Panic 之后就不再继续执行catchExceptions()time.Sleep(3 * time.Second)if num == 5 {fmt.Printf("==== end main =====\r\n")return}}
}
一般的建议是在请求来源入口处的函数或者关键路径上实现这么一段代码进行捕获,这样,只要通过这个入口出现的异常都能被捕获,并打印详细日志。同时,为了保证 goroutine
能够继续执行任务,因此还要考虑当出现 panic
被捕获之后,是否有主动循环或者被动触发来重新执行任务。
7. 如何应对 panic
7.1 评估程序对 panic 的忍受度
Go
标准库提供的 http server
采用的是,每个客户端连接都使用一个单独的 Goroutine
进行处理的并发处理模型。也就是说,客户端一旦与 http server
连接成功,http server
就会为这个连接新创建一个 Goroutine
,并在这 Goroutine
中执行对应连接(conn
)的 serve
方法,来处理这条连接上的客户端请求。
无论在哪个 Goroutine
中发生未被恢复的 panic
,整个程序都将崩溃退出。所以,为了保证处理某一个客户端连接的 Goroutine
出现 panic
时,不影响到 http server
主 Goroutine
的运行,Go
标准库在 serve
方法中加入了对 panic
的捕捉与恢复,下面是 serve
方法的部分代码片段:
// $GOROOT/src/net/http/server.go
// Serve a new connection.
func (c *conn) serve(ctx context.Context) {c.remoteAddr = c.rwc.RemoteAddr().String()ctx = context.WithValue(ctx, LocalAddrContextKey, c.rwc.LocalAddr())defer func() {if err := recover(); err != nil && err != ErrAbortHandler {const size = 64 << 10buf := make([]byte, size)buf = buf[:runtime.Stack(buf, false)]c.server.logf("http: panic serving %v: %v\n%s", c.remoteAddr, err, buf)}if !c.hijacked() {c.close()c.setState(c.rwc, StateClosed, runHooks)}}()... ...
}
你可以看到,serve
方法在一开始处就设置了 defer
函数,并在该函数中捕捉并恢复了可能出现的 panic
。这样,即便处理某个客户端连接的 Goroutine
出现 panic
,处理其他连接 Goroutine
以及 http server
自身都不会受到影响。
这种局部不要影响整体的异常处理策略,在很多并发程序中都有应用。并且,捕捉和恢复 panic
的位置通常都在子 Goroutine
的起始处,这样设置可以捕捉到后面代码中可能出现的所有 panic
,就像 serve
方法中那样。
7.2 提示潜在 bug
在 json
包的 encode.go
中也有使用 panic
做潜在 bug
提示的例子:
// $GOROOT/src/encoding/json/encode.go
func (w *reflectWithString) resolve() error {... ...switch w.k.Kind() {case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:w.ks = strconv.FormatInt(w.k.Int(), 10)return nilcase reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:w.ks = strconv.FormatUint(w.k.Uint(), 10)return nil}panic("unexpected map key type")
}
这段代码中,resolve
方法的最后一行代码就相当于一个“代码逻辑不会走到这里”的断言。一旦触发“断言”,这很可能就是一个潜在 bug。
我们也看到,去掉这行代码并不会对 resolve
方法的逻辑造成任何影响,但真正出现问题时,开发人员就缺少了“断言”潜在 bug
提醒的辅助支持了。在 Go
标准库中,大多数 panic
的使用都是充当类似断言的作用的。
Go 学习笔记(19)— 函数(05)[如何触发 panic、触发 panic 延迟执行、panic 和 recover 的关系]相关推荐
- Python学习笔记19:列表 III
Python学习笔记19:列表 III 其实这篇笔记标题应该是列表扩展,从列表开始,将涵盖Python中的序列容器. 关于列表的基础知识,可以看我的前两篇文章: Python学习笔记1:列表. Pyt ...
- Linux 学习笔记19 信号
Linux 学习笔记19 信号 信号 信号概述 为什么要是使用信号--为了实现进程的有序退出 信号是进程运行过程中,由自身产生或者由进程外部发来的消息.信号是硬件中断的软件模拟(软中断) signal ...
- python函数是一段具有特定功能的语句组_Python学习笔记(五)函数和代码复用
本文将为您描述Python学习笔记(五)函数和代码复用,具体完成步骤: 函数能提高应用的模块性,和代码的重复利用率.在很多高级语言中,都可以使用函数实现多种功能.在之前的学习中,相信你已经知道Pyth ...
- Ext.Net学习笔记19:Ext.Net FormPanel 简单用法
Ext.Net学习笔记19:Ext.Net FormPanel 简单用法 FormPanel是一个常用的控件,Ext.Net中的FormPanel控件同样具有非常丰富的功能,在接下来的笔记中我们将一起 ...
- springmvc学习笔记(19)-RESTful支持
springmvc学习笔记(19)-RESTful支持 标签: springmvc springmvc学习笔记19-RESTful支持 概念 REST的样例 controller REST方法的前端控 ...
- Python学习笔记:函数(Function)
Python学习笔记:函数(Function) 一.函数基本概念 函数是Python里组织与重用代码最重要的方法.一般来说,如果你期望多次重复相同或相似的代码,写一个可重用的函数可能是值得的.函数通过 ...
- php中声明一个函数,php学习笔记之 函数声明
/* 函数定义: * 1.函数是一个被命名的 * 2.独立的代码段 * 3.函数执行特定任务 * 4.并可以给调用它的程序返回一个值 * * 函数的优点: * 1.提高程序的重用性 * 2.提高程序的 ...
- Matlab学习笔记 figure函数
Matlab学习笔记 figure函数 matlab中的 figure 命令,能够创建一个用来显示图形输出的一个窗口对象.每一个这样的窗口都有一些属性,例如窗口的尺寸.位置,等等.下面一一介绍它们. ...
- 区块链学习笔记19——ETH难度调整
区块链学习笔记19--ETH难度调整 学习视频:北京大学肖臻老师<区块链技术与应用> 笔记参考:北京大学肖臻老师<区块链技术与应用>公开课系列笔记--目录导航页 前面学过,比特 ...
- JAVA学习笔记五---函数
JAVA学习笔记五---函数 5.1 方法的学习 编写一个程序,求圆的周长和面积. package practice; /*** 编写一个程序,求圆的周长和面积.* @author iszhangyo ...
最新文章
- MySQL的4中隔离级别
- VMware虚拟机中CentOS根分区的扩展
- 数组 ——求出一组数的最大值(用数组实现)
- fastbin attack攻击中关于 malloc__hook
- angular_ui-router ——依赖注入
- 【Linux网络编程】 网络协议入门
- [SCOI2009]最长距离
- monterey系统怎么降级?macOS Monterey系统降回Big Sur的详细教程
- ELK下Kibana和Elasticsearch之间相互TLS身份验证
- nyoj 309 bobsledding 即河南省第四届大学生程序设计大赛第七题
- 基于matlab单目视觉焊缝跟踪系统,基于激光线结构光3D视觉的机器人轨迹跟踪方法与应用...
- 全网首发:linux任务栏分组的研究
- 中文简体繁体转换(JS 字符串 简体转繁体 繁体转简体)
- [Daozy][区块链 EOS 课程]第2课 EOS编译和启动
- 门函数卷积_卷积及其应用
- 说说域名、二级域名和主机名的联系区别
- Arduino驱动HDC1080测量温湿度
- Vue中时间日期格式化
- java基础知识面试题(41-95)
- ubuntu IPV6及作为路由分配【笔记】
热门文章
- 自制青蛙跳台阶小游戏~
- 2021-2027年中国医疗美容市场研究及前瞻分析报告
- 并发 vs 并行 (Concurrency Is Not Parallelism)
- 判断某数组是不是二叉树的前序遍历序列 python递归
- 使用 Pytorch 实现 skip-gram 的 word2vec
- LeetCode简单题之判断矩阵经轮转后是否一致
- SoC(System on chip)与NoC(network-on-chip)
- ALD技术,相机去噪,图像传感器
- 2021年大数据Flink(三十九):​​​​​​​Table与SQL ​​​​​​总结 Flink-SQL常用算子
- getCacheDir() 和 getFilesDir() 的区别