Go 语言程序错误分为两种:
- error ,程序员可以预料到的,安错误种类加以处理;
- panic ,无法预料到的;
典型原因
引起 panic 的常见操作包括:
- 数组越界访问
- 类型断言失败
- 访问空指针
- 互斥锁错误调用
- 向已关闭的 channel 发送数据
- etc
越界异常
很多初级程序员经常这样访问数组或切片的最后一个元素:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
package main
import (
"fmt"
)
func main() {
names := []string{
"lobster",
"sea urchin",
"sea cucumber",
}
fmt.Println("My favorite sea creature is:", names[len(names)])
}
|
len 计算元素总数,但由于元素下标是从 0 开始的,最后一个元素下标应该是 len(names)-1
。例子中的程序员忘了减一,因此 Go 程序会抛出越界异常。
空指针
访问值为 nil 的指针变量,也会抛出运行时异常:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
package main
import (
"fmt"
)
type Pet struct {
Name string
}
func (p *Pet) Greet() {
fmt.Printf("Hi! My name is %s\n", p.Name)
}
func main() {
coco := Pet{"Coco"}
coco.Greet()
var unknown *Pet
unknown.Greet()
}
|
这个例子第一个 Greet 调用是正常的,第二个因指针变量为初始化而抛异常:
1
2
3
4
5
6
7
8
9
|
Hi! My name is Coco
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x48218e]
goroutine 1 [running]:
main.(*Pet).Greet(...)
/tmp/sandbox2349740722/prog.go:12
main.main()
/tmp/sandbox2349740722/prog.go:20 +0x8e
|
panic上下文
panic 异常上下文信息由两部分组成:
- 异常原因描述,
panic:
部分;
- 栈追踪信息,据此可定位到引发异常的代码位置;
1
2
3
4
5
|
panic: runtime error: index out of range [3] with length 3
goroutine 1 [running]:
main.main()
/tmp/sandbox3385861673/prog.go:13 +0x1b
|
panic函数
有时需要自己抛异常,这时可以调用内建函数 panic :
1
2
3
4
5
6
7
8
9
|
package main
func main() {
raise()
}
func raise() {
panic("oh no!")
}
|
defer函数
Go 语言引入了 defer 关键字,可以在函数结束时做一些清理工作。那函数抛异常时,defer 还会执行吗?从理论上分析,应该要执行,不然可能导致资源无法释放。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
package main
func main() {
raise()
}
func raise() {
defer greet()
fmt.Println("before panic")
panic("oh no!")
}
func greet() {
fmt.Println("defer is running")
}
|
执行这个例子,输出内容大致如下:
1
2
3
|
before panic
defer is running
panic: oh no!
|
这说明 defer 在抛异常的时候也会执行!
捕获异常
由于 defer 一定会执行,可以利用它调用 recover 来捕获异常。recover 是一个可以直接调用内建函数,它会捕获调用栈上抛出来的异常。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
package main
func main() {
fmt.Println("value return:", raise())
}
func raise() int {
defer catch()
panic("oh no!")
return 10
}
func catch() {
if err := recover(); err != nil {
fmt.Println("panic caught:", err)
}
}
|
这个例子利用 defer 关键字执行 catch 函数捕获异常,这样程序就不会异常退出了。注意到,由于 raise 函数并非正常执行完毕,因此不是按预期返回 10 ,而是返回 int 的零值。
panic报告
笔者在维护一个微服务模块,里面开了很多协程跑定时任务。由于代码提交质量有限,有时定时任务 panic 挂了都没发现。为此,我想实现一个 panic 报告函数,来捕获并记录 panic :
1
2
3
4
5
6
7
8
|
func report_panic() {
if err := recover(); err != nil {
stack := debug.Stack()
fmt.Println(err, stack)
// 将上下文信息写到数据库:err stack
// 推送告警消息
}
}
|
获得 panic 异常信息和堆栈信息后,可以将它们保存到数据库,或通过告警消息发出来。
【小菜学Go语言】系列文章首发于公众号【小菜学编程】,敬请关注: