引子
在Java中,我们可以通过throw、try{}catch{}finally{}进行方便的异常处理,在C++中稍微复杂一些,没有finally语法,因此在遇到程序发生异常但需要关闭资源的时候,在C++中通常两种做法,第一种也是最常用的一种是使用RAII,即Resource Aquisition Is Initialization就是将资源封装成一个类,将资源的初始化封装在构造函数里,释放封装在析构函数里。要在局部使用资源的时候,就实例化一个local object。在抛出异常的时候,由于local object脱离了作用域,自动调用析构函数,这样就保证资源被释放。例如:1
2
3
4
5
6
7try {
File f("xxx.ttt");
//other file operation
}//File pointer is released here
catch {
//exception process
}
另一种是沿用的C语言对于异常的处理方法:使用goto语句,将需要释放的资源变量都声明在函数开头部分,并在函数末尾统一释放资源,当函数需要退出时,使用goto语句跳转到指定位置完成资源清理工作,而不调用return直接返回:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17int main(){
int a = 0;
int b = 0;
printf("请输入两个值:\n");
printf("a = ");
scanf("%d",&a);
printf("b = ");
scanf("%d",&b);
if(b==0){
goto Error;
}
printf("a/b = %d\n",a/b);
return 0;
Error:
printf("除数不能为0,程序异常退出!\n");
exit(-1);
}
Java处理异常方便,但是它将异常与控制结构混在一起会很容易使得代码变得混乱,开发者也容易滥用异常,对性能会造成影响,代码也不符合Go“简洁优雅”的设计理念;C/C++处理异常需要开发者遵守一套编码规范,如果不遵守的话,维护就会成为很大的问题,Go站在前人肩膀上看问题,提出了一套新的解决方案。
defer
defer就相当于finally,defer的特性是,不管会不会发生异常,在函数返回之前,先调用defer函数,如果有多个defer语句,按照先进后出的方式进行执行,需要注意的是,defer语句中的变量,在defer声明时就决定了。
例如使用defer时的一个坑:
1 | func main() { |
上面我们说到,defer会在函数返回之前被调用,但这并不意味着defer是在return之前被执行,是这样吗?看下面的例子:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15package main
import "fmt"
func test() (res int) {
res = 1
defer func() {
res++
}()
return 0
}
func main() {
fmt.Print(test()) // output: 1
}
这样的原因是,return不是原子性的,它包含的过程如下:
- 给返回值赋值;
- 调用RET返回指令并传入返回值,而RET则会检查defer是否存在,若存在就先逆序插播defer语句,最后RET携带返回值退出函数。
在这个例子中的话,res = 1 -> res = 0 -> res++,所以res返回值最终是1
panic和recover
panic,恐慌,也就是抛出异常;
recover,恢复,也就是从异常中恢复状态;
在go里,panic和recover是抛异常和捕获异常的解决方案,panic相当于throw Exception,recover和defer结合起来相当于try…catch…
先看一个手动引起异常的例子:1
2
3
4
5
6
7func main() {
a := []int{1, 2, 3}
panic("触发异常")
fmt.Print(a[2])
}
-----output-----
panic: 触发异常
panic是golang的内建函数,panic会中断函数F的正常执行流程, 从F函数中跳出来, 跳回到F函数的调用者. 调用者会继续向上跳出, 直到当前goroutine返回,在这个控制器传播过程中,panic详情会积累和完善,并在程序终止之前打印出来。在跳出的过程中, 进程会保持这个函数栈. 当goroutine退出时, 程序会crash。
要注意的是, F函数中的defered函数会正常执行, 按照上面defer的规则。
同时引起panic除了我们主动调用panic之外, 其他的任何运行时错误, 例如数组越界都会造成panic,比如:1
2
3
4
5
6func main() {
a := []int{1, 2, 3}
fmt.Print(a[3])
}
-----output-----
panic: runtime error: index out of range [3] with length 3
recover也是golang的一个内建函数, 其实就是try catch。
不过需要注意的是:
- recover如果想起作用的话, 必须在defered函数中使用。
- 在正常函数执行过程中,调用recover没有任何作用, 他会返回nil。如这样:fmt.Println(recover()) 。(当然了,没捕获到异常肯定是nil了==)
- 如果当前的goroutine panic了,那么recover将会捕获这个panic的值,并且让程序正常执行下去。不会让程序crash。
看一个正常的捕获异常的例子:无论foo()是否触发了错误处理流程,defer函数都会在函数退出时得到执行,如果没抛出异常,那么r就是nil,如果抛出了异常,recover就能捕获它、处理它。1
2
3
4
5
6defer func() {
if r := recover(); r != nil {
log.Printf("Runtime error caught: %v", r)
}
}()
foo()