Go语言异常处理机制

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语言】系列文章首发于公众号【小菜学编程】,敬请关注:

【小菜学Go语言】系列文章首发于公众号【小菜学编程】,敬请关注: