返古的错误处理方式,是 Go 被谈及最多的内容之一。

error

官方推荐的标准做法是返回 error 状态。

func Scanln(a ...interface{}) (n int, err error)

标准库将 error 定义为接口类型,以便实现自定义错误类型。

type error interface {
	Error() string
}

按惯例,error 总是最后一个返回参数。标准库提供了相关创建函数,可方便地创建包含简单错误文本的 error 对象。

var errDivByZero = errors.New("division by zero")
 
func div(x, y int) (int, error) {
	if y == 0 {
		return 0, errDivByZero
	} 
 
	return x / y, nil
} 
  
func main() { 
	z, err := div(5, 0) 
	if err == errDivByZero { 
		log.Fatalln(err)
	} 
 
	println(z)
}

应通过错误变量,而非文本内容来判定错误类别。

错误变量通常以 err 作为前缀,且字符串内容全部小写,没有结束标点,以便于嵌入到其他格式化字符串中输出。

与 errors.New 类似的还有 fmt.Errorf,它返回一个格式化内容的错误对象。

某些时候,我们需要自定义错误类型,以容纳更多上下文状态信息。这样的话,还可基于类型做出判断。

type MyError struct {} // 自定义错误类型
 
func (*MyError) Error string { // 实现 error 接口方法
	return "my error"
}
 
func (*MyError) Msg string {
	return "my error msg"
}
 
func bar() {
	return MyError{}
}
 
func main() {
	err := bar()
 
	if err != nil {
		switch e := err.(type) { // 根据类型匹配
		case MyError:
			fmt.Println(e, e.Msg())
		default:
			fmt.Println(e)
		}
	}
}

自定义错误类型通常以 Error 为名称后缀。在用 switch 按类型匹配时,注意 case 顺序。应将自定义类型放在前面,优先匹配更具体的错误类型。

在正式代码中,我们不能忽略error返回值,应严格检查,否则可能会导致错误的逻辑状态。

调用多返回值函数时,除 error 外,其他返回值同样需要关注,以 os.File.Read 方法为例,它会同时返回剩余内容和 EOF。

大量函数和方法返回 error,使得调用代码变得很难看,一堆堆的检查语句充斥在代码行间。解决思路有:

  • 使用专门的检查函数处理错误逻辑(比如记录日志),简化检查代码。
  • 在不影响逻辑的情况下,使用 defer 延后处理错误状态(err 退化赋值)。
  • 在不中断逻辑的情况下,将错误作为内部状态保存,等最终“提交”时再处理。

panic,recover

与 error 相比,panic/recover 在使用方法上更接近 try/catch 结构化异常。

func panic(v interface{})
 
func recover() interface{}

比较有趣的是,它们是内置函数而非语句。panic 会立即中断当前函数流程,执行延迟调用。而在延迟调用函数中,recover 可捕获并返回 panic 提交的错误对象。

func main() { 
	defer func() { 
		if err := recover(); err != nil { // 捕获错误
			log.Fatalln(err) 
		} 
	}()
 
	panic("i am dead") // 引发错误
	println("exit.") // 永不会执行
}

因为 panic 参数是空接口类型,因此可使用任何对象作为错误状态。而 recover 返回结果同样要做转型才能获得具体信息。

无论是否执行 recover,所有延迟调用都会被执行。但中断性错误会沿调用堆栈向外传递,要么被外层捕获,要么导致进程崩溃。

func test() {
	defer println("test.1")
	defer println("test.2")
 
	panic("i am dead")
} 
  
func main() {
	defer func() {
		log.Println(recover()) // 捕获
	}()
 
	test()
}

连续调用 panic,仅最后一个会被 recover 捕获。

func main() {
	defer func() {
		for { 
			if err := recover(); err != nil { 
				log.Println(err) 
			} else {
				log.Fatalln("fatal") 
			} 
		} 
	}() 
 
	defer func() { 
		panic("you are dead") // 类似重新抛出异常(rethrow)
	}() // 可先recover捕获,包装后重新抛出 
 
	panic("i am dead") 
}

输出:

you are dead
fatal

在延迟函数中 panic,不会影响后续延迟调用执行。而 recover 之后 panic,可被再次捕获。

另外,recover 必须在延迟调用函数中执行才能正常工作。

func catch() { 
	log.Println("catch:", recover()) 
} 
  
func main() { 
	defer catch() // 捕获
	defer log.Println(recover()) // 失败,输出:nil
	defer recover() // 失败
 
	panic("i am dead")
}

考虑到 recover 特性,如果要保护代码片段,那么只能将其重构为函数调用。(对比 try-catch,需要多包一层 func)

func test(x,y int) { 
	z := 0
	
	func() { // 利用匿名函数try:z = x / y 
		defer func() {
			if recover() != nil { 
				z = 0
			} 
		}()
 
		z = x / y
	}()
 
	println("x / y =", z) 
}
 
func main() { 
	test(5,0) 
}

调试阶段,可使用 runtime/debug.PrintStack 函数输出完整调用堆栈信息。

import "runtime/debug"
 
debug.PrintStack()

建议:除非是不可恢复性、导致系统无法正常工作的错误,否则不建议使用 panic。

例如:文件系统没有操作权限,服务端口被占用,数据库未启动等情况。