Golang 中的错误处理是一个被大家经常拿出来讨论的话题(另外一个是泛型),这里简单介绍其使用方法。
简介
GoLang 没有提供像 Java 的 try...catch
异常处理方式,而是通过函数返回值逐层往上,通过这种方式鼓励工程师显式检查错误而非忽略错误,好处是避免漏掉本应处理的错误,但使得代码比较繁杂。
如下是一个简单的示例。
package main
import (
"errors"
"fmt"
)
func Sqrt(f float64) (float64, error) {
if f < 0 {
return 0, errors.New("math: square root of negative number")
}
return f, nil
}
func main() {
if res, err := Sqrt(-1); err != nil {
fmt.Println(err)
} else {
fmt.Println(res)
}
}
在函数中,通过 errors.New()
新建并返回一个错误信息;在调用方,如果返回的结果为 nil 则输出错误,在 fmt
中处理 error 时会调用 Error()
方法输出错误信息。
在 GoLang 中有如下的使用方式。
简单定义
可以在某个包里通过 errors.New()
新建一个对象,在代码其它地方可以直接引用,比较常用的是 io.EOF
,用于判断 io 读取是否结束,其定义为。
package io
var EOF = errors.New("EOF")
在代码中可以通过如下方式使用。
if _, err := file.Read(buf); err == nil {
log.Printf("%s", buf)
} else if err == io.EOF {
log.Println("Read all file")
return nil
} else {
log.Printf("Read file failed, %v.\n", err)
return err
}
如果要打印额外信息,也可以通过 fmt.Errorf()
格式化输出新的错误信息。
Wrap
原始在 github.com/pkg/errors
包中使用,后来在 Go 1.13
中新增了 Error Wrapping 的功能,不过没有提供 Wrap
函数,而是通过扩展 fmt.Errorf
的 %w
参数实现。
除此之外,为了便于处理嵌套错误,同时包含了如下三个工具函数:
Unwrap()
返回被嵌套的错误,如果嵌套多层只返回第一层错误。Is()
原来通过err == io.EOF
判断,引入 Wrap 后会判断多层嵌套是否有该错误。As()
原来通过断言判断错误类型,通过该方法可以处理嵌套。
如下是简单示例。
// 嵌套错误
ori := errors.New("original error")
err := fmt.Errorf("wrapped with: %w", ori)
fmt.Println(err) // wrapped with: original error
fmt.Println(errors.Unwrap(err)) // original error
// 判断错误是否为某个具体错误类型
if err == io.EOF {
return err
}
if errors.Is(err, io.EOF) {
return err
}
// 进行类型转换,原始只能通过类型断言判断
if perr, ok := err.(*os.PathError); ok {
fmt.Println(perr.Path)
}
var perr *os.PathError
if errors.As(err, &perr) {
fmt.Println(perr.Path)
}
实现接口
在 GoLang 标准包中提供的错误处理功能是一个接口,定义如下。
type error interface {
Error() string
}
也就是说,对于内置的 error
来说,只要实现该接口即可,例如在 net
包中定义了与网络相关的错误。
package net
// An Error represents a network error.
type Error interface {
error
Timeout() bool // Is the error a timeout?
Temporary() bool
}
// A ParseError is the error type of literal network address parsers.
type ParseError struct {
Type string
Text string
}
func (e *ParseError) Error() string { return "invalid " + e.Type + ": " + e.Text }
func (e *ParseError) Timeout() bool { return false }
func (e *ParseError) Temporary() bool { return false }
那么在代码中就可以通过如下逻辑进行处理。
if e, ok := err.(net.Error); ok && e.Temporary() {
time.Sleep(1e9)
continue
} else {
return err
}
除了上述的接口方式定义,如果比较简单,还可以直接使用结构体,这样可以直接使用结构体中定义的公共变量。
总结
如上,在 GoLang 中有很多种方式来声明错误类型:
errors.New()
简单静态字符串错误;fmt.Errorf()
格式化的错误字符串;- 单独实现有
Error()
方法的自定义类型; pkg/errors
中的 Wrapped Errors 机制。
在选择时,一般基于如下的考量:A) 对于简单的错误信息,可以使用 errors.New()
或者 fmt.Errorf()
;B) 而当用户需要检测并处理这一错误时,可以使用自定义类型并实现 Error()
接口,然后用户使用断言判断;也可以通过 errors.New()
新建变量,然后用户判断。
异常
错误和异常是两个不同的概念,非常容易混淆,通常是将所有类型其看做错误,即使程序中可能有异常抛出,也将异常及时捕获并转换成错误。
- 错误和异常如何区分?错误通常是指业务正常处理流程中出现了问题,例如文件打开失败、入参为空指针等;而异常非意料中的问题出现,例如内存申请失败等。
- 错误处理的方式有哪几种?GoLang 采用类似 C 的错误码,用于逐层返回,直到被处理,业务中需要根据具体类型进行处理。
- 什么时候需要使用异常终止程序?通常是程序受到了影响,无法正常运行时,不如就直接退出程序,例如空指针、下标越界等。
- 什么时候需要捕获异常?同样要具体分析,可以在任务处理的最外层捕获,这样任务失败不影响其它任务执行,例如 HTTP 请求。
Go 追求的是简洁优雅,没有提供传统的 try ... catch ... finally
这种异常处理方式,引入的是 defer
panic
recover
。也就是在 Go 中抛出一个 panic
异常,然后在 defer
中通过 recover
捕获这个异常,然后正常处理。
机制介绍
其中 recover()
是 GoLang 的内建函数,可以让进入 panic
流程中的协程恢复过来,该函数仅在 defer
中有效,正常调用 recover()
会返回 nil
并且没有其它任何效果,如果当前协程执行了 panic
,那么调用 recover()
可以捕获到 panic
的输入值,并且恢复正常执行。
正常是无需对 panic
的程序做任何处理,但有时需要从中恢复,至少可以在程序崩溃前做些操作。
示例
Go 对待异常 (准确说是panic) 态度是:没有全面否定异常的存在,但极不鼓励多用异常。
package main
import "fmt"
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
fmt.Println("Process panic done")
}()
foobar()
}
func foobar() {
fmt.Println("Before panic")
panic("Panicing ...")
fmt.Println("After panic")
}
通过 go run main.go
执行会输出如下内容。
Before panic
Panicing ...
Process panic done
也就是说,在 Panic
之后的内容不会再执行。
最佳实践
显示区分
可以通过函数名显示区分错误和异常,例如,在 regexp
包中有两个函数 Compile
MustCompile
,它们的声明如下:
func Compile(expr string) (*Regexp, error)
func MustCompile(str string) *Regexp
同样的功能,不同的设计:
Compile()
基于错误处理设计,适用于用户输入场景,当用户输入的正则表达式不合法时,该函数会返回一个错误。MustCompile()
基于异常处理设计,适用于硬编码场景,调用者明确知道输入不会引起函数错误,如果出现则直接触发异常。
也就是说,必须要明确什么是错误什么是异常,否则很容易出现一切皆错误或一切皆异常的情况。