[golang] 错误处理
[golang] 错误处理
字符串类型的错误
// simple string-based error
err1 := errors.New("math: square root of negative number")
// with formatting
err2 := fmt.Errorf("math: square root of negative number %g", x)
自定义错误
自定义错误可携带额外信息,比如错误码等。
先定义一个错误码枚举:
// 定义枚举前需要先定义类型
type Status int
// 定义枚举
const (
InvalidLogin = iota + 1
NotFound
)
然后使用上面的枚举来实现自定义错误:
type StatusErr struct {
Status Status
Message string
}
func (se StatusErr) Error() string {
return se.Message
}
测试:
func foo() error {
id := 1000
return StatusErr{
Status: InvalidLogin,
Message: fmt.Sprintf("status error with user id %d", id),
}
}
func main() {
err := foo()
switch e := err.(type) {
case StatusErr:
fmt.Println("error with status code:", e.Status)
default:
fmt.Println(e)
}
}
// error with status code: 1
上面示例代码中,注意两点:
- 即使使用自定义的错误,也应该返回内置错误类型
error
,这样调用方可以不依赖具体的错误类型,更加解耦 - 当需要时,再还原原来的类型以获取其中的额外信息
哨兵错误
有一种错误,Sentinel 错误,约定以 Err
开头,定义在包作用域内,当调用方接收到该类型错误后,表示程序无需继续往下,应该终止执行。
比如标准库中解压时文件类型不对的错误 ErrorFormat
:
func main() {
data := []byte("blahblah...")
notAZipFile := bytes.NewReader(data)
_, err := zip.NewReader(notAZipFile, int64((len(data))))
if err == zip.ErrFormat {
fmt.Println("invalid file type!")
}
}
查看标准库中的实现,可看到其就是普通的 error
类型,只是命名按照了上述约定:
// /xxx/go/1.16.5/libexec/src/archive/zip/reader.go
var (
ErrFormat = errors.New("zip: not a valid zip file")
ErrAlgorithm = errors.New("zip: unsupported compression algorithm")
ErrChecksum = errors.New("zip: checksum error")
)
Error 的包装
接收到 error 后,可添加点信息在上面,再将其返回出去。这样可形成一条错误链。
前面涉及到的 fmt.Errorf
支持一个 %w
(wrap)参数,可接入一个 error
类型从而在其上面进行二次包装。
func bar() error {
err := foo()
return fmt.Errorf("some more info on err, the source err is :%w", err)
}
%v
(value) 则只是将原错误信息的文本形成新文本,而不会有包装操作。
对应地,通过 errors.Unwrap
可解出被封装的原始错误信息:
func main() {
err := bar()
if err != nil {
fmt.Println(err)
if sourceErr := errors.Unwrap(err); sourceErr != nil {
fmt.Println(sourceErr)
}
}
}
// some more info on err, the source err is :status error with user id 1000
// status error with user id 1000
自定义错误要支持 Unsrap
需要实现该方法:
type StatusError struct {
Status Status
Message string
+ err error
}
+ func (se StatusError) Unwrap()error{
+ return se.err
+ }
Is
和 As
既然错误可以二次包装,那么问题来了,如果对哨兵错误进行了包装,调用方如何知道错误中包含了哨兵错误呢。Go 中提供了两个方法 [errors.Is](http://errors.Is)
和 [errors.As](http://errors.As)
来解决这个问题。
func fileChecker(name string) error {
f, err := os.Open(name)
if err != nil {
return fmt.Errorf("in fileChecker: %w", err)
}
defer f.Close()
return nil
}
func main() {
err := fileChecker("xxx.txt")
if err != nil {
if errors.Is(err, os.ErrNotExist) {
fmt.Println("file not exit")
}
}
}
内部实现上,errors.Is
是通过不断 Unwrap
错误并与之进行对比来判断的,所以,自定义错误要支持对比,需要实现 Is
方法:
type MyErr struct {
Status int
}
func (me MyErr)Error()string{
return fmt.Sprintf("error code: %d",me.Status)
}
func (me MyErr) Is(target error)bool{
if me2,ok:=target.(MyErr);ok{
return reflect.DeepEqual(me,me2)
}
return false
}
As
判断一个错误是不是你要找的类型,该方法接收两个参数,一个是具体的错误,另一个是想要找的类型的一个指标,成功的话,说明在错误链上打到了对应类型错误并赋值给了这个指针。
func main() {
err:=AFuncThatReturnsAnErr()
var myErr MyErr
if errors.As(err,&myErr){
fmt.Println(myErr.Code)
}
}
所以前面在讲自定义错误的时候,除了使用 err.(type)
来还原错误本身的类型,也可以用这里提到的 As
。
同理,也可以自己实现自定义错误的 As
方法来决定其中的细节,不过一般场景不涉及。
总结:
- 使用
[errors.Is](http://errors.Is)
来查找具体某个错误值 - 使用
[errors.As](http://errors.As)
来判断具体某个错误类型
结合 defer
来包装错误
函数中如果有多个地方都需要进行同样的错误包装,比如像下面这样:
func foo(i int)(string,error) {
_,err:=f1(i)
if err=nil{
return "",fmt.Errorf("in foo:%w",err)
}
_,err:=f2(i)
if err=nil{
return "",fmt.Errorf("in foo:%w",err)
}
_,err:=f3(i)
if err=nil{
return "",fmt.Errorf("in foo:%w",err)
}
return "",nil
}
联想到之前介绍过的 defer
可做一些收尾工作,可利用其在函数返回时来统一进行错误包装,优化掉这里的冗余代码:
func foo(i int)(_ string,err error) {
defer func() {
if err!=nil{
err=fmt.Errorf("in foo:%w",err)
}
}()
_,err:=f1(i)
if err=nil{
return "",err
}
_,err:=f2(i)
if err=nil{
return "",err
}
return f3(i)
}
注意使用 defer
进行返回时,需要为返回变量声明名称,这样才能在 defer
中使用。
发生 panic 及恢复
Go 程序中发生异常时会生成 panic,比如试图访问超出 slice 边界的元素,内存溢出等。
除了 runtime 会发生 panic,程序中也可根据需要来生成:
func genPanic(msg string) {
panic(msg)
}
func main() {
genPanic("blah")
}
运行结果:
$ go run main.go
panic: blah
goroutine 1 [running]:
main.genPanic(...)
/xxx/main.go:4
main.main()
/xxx/main.go:8 +0x50
exit status 2
make: *** [run] Error 1
可通过 recover
来处理 panic,结合 defer
来用,让程序优雅退出而不是崩溃:
func div60(i int) {
defer func() {
if v := recover(); v != nil {
fmt.Println(v)
}
}()
fmt.Println(60 / i)
}
func main() {
for _, val := range []int{1, 2, 0, 6} {
div60(val)
}
}
运行结果:
go run main.go
60
30
runtime error: integer divide by zero
10
与其他语言的 try catch 不同,发生 panic 后不建议通过 recover 让程序还继续运行,而是需要根据 panic 进行对应善后。比如,如果是内存溢出,在 recover 时及时落日志上报监控保存现场然后通过 os.Exit(1)
退出程序。
不应该通过 panic recover 模式来获取出错的堆栈信息,recover 只是单纯提供一种在发生错误时,将信息打印出来,然后程序能够正常继续而不崩掉的机制。如果想打印出错误时的堆栈信息,一是可以通过包装错误自己实现,二是可用现成三方库。使用 fmt.Printf
时,也可通过 %+v
打印 stack trace,但注意此时会打印程序文件的绝对路径,真实生产环境中一般想要隐藏掉这些信息,可在编译时加上 -trimpath
参数。