error的处理方式
前言:
此文是观看毛剑老师的《GO进阶训练营》中的《异常处理》的个人理解和总结,如果有不对的地方,还望指正,谢谢🙏。
error和exception
exception
无法区分普通异常和致命异常,始终需要使用try...catch
命令去捕捉异常,在自己编码中经常会去考虑是否对其异常捕获,甚至有时会对异常catch
多次。
而go
中的error
利用多返回值,使用普通error
和panic
分别代表了普通异常和致命异常,更加明确了对异常的处理。
一、什么是error
go
中的error
实际上是实现了error
接口的普通类型,这样可以很容易实现自定义的error
。
// https://golang.org/src/builtin/builtin.go
type error interface {
Error() string
}
官方标准库中的errors
:
// https://golang.org/src/errors/errors.go
package errors
func New(text string) error {
return &errorString{text}
}
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}
其中New
方法返回errorString
内存地址是为了避免在使用相同errors
包时,不同的错误因为传入的文字信息相同而相等。
package main
import "fmt"
type myError struct {
s string
}
func (m myError) Error() string {
return m.s
}
func New(text string) error {
return myError{text}
}
func main() {
aErr := New("error")
bErr := New("error")
if aErr == bErr {
fmt.Println("aErr == bErr") // aErr == bErr
}
}
上述代码会认为分别New
出来的error
时相等的,这样明显是不正确的,当修改返回的error
是内存地址时,即使相同文字也不会相等。
二、error和panic使用场景
error
一般用于可遇见的普通异常,比如参数错误、查询错误等异常;而panic
一般用于强依赖的第三方、配置信息发生错误,比如db无法连接、配置参数不存在等异常,但是使用方式需要根据实际代码逻辑进行判断。
三、error的几种使用方式
1、Sentinal Error
Sentinal Error
是预先在包中定义的特定错误,比如io
包中的EOF
:
// https://golang.org/src/io/io.go
var EOF = errors.New("EOF")
io.EOF
是用于在文件读取时没有内容返回的错误,所以在外部接收到error
时需要判断是否等于io.EOF
:
if err != nil {
if err == io.EOF {
// do something
}
}
使用Sentinal Error
有以下不足:
无法携带多余的上下文信息,如果使用
fmt.Errorf
添加自定义信息,返回的错误无法再与Sentinal Error
进行比较。使包与包之间产生耦合,假如存在
Sentinal Error
的包是项目的基础包,引入这个基础的其他包都必须导入这些错误值,增加了耦合性的同时基础包也无法再对这些错误值进行相关修改。
所以Sentinal Error
尽量避免在基础包(或者很多包引用的包)中使用。
2、Error Type
Error Type
是实现了error
接口的自定义类型,可以保留底层的错误同时增加上下文信息。
例如myError
类型记录了文件和行号的上下文:
package main
import (
"fmt"
)
type myError struct {
s string
file string
line int
}
func (m *myError) Error() string {
return fmt.Sprintf("%s-%d: %s", m.file, m.line, m.s)
}
func New(text, file string, line int) error {
return &myError{text, file, line}
}
func main() {
err := do()
switch err := err.(type) {
case nil:
// no error
case *myError:
fmt.Printf("error occurred: %v\n", err.Error())
default:
// unknown error
}
}
func do() error {
return New("something error", "main.go", 27)
}
Error Type
可以使用断言转换成这个类型,来获取上下文信息,但是Error Type
仍然存在与其他包产生耦合的问题。
3、Opaque errors(非透明错误处理)
Opaque erros
只关心返回的错误而不是错误内容。
func fn() error {
i, err := do()
if err != nil {
return err
}
// do something
}
Opaque errors
的好处减少了代码间的耦合性,但是在很多时候仍然需要确认返回错误的性质,以便进一步处理,比如网络请求是超时还是返回错误,此时推荐使用断言错误的行为,而不是断言错误的类型。
例如:
定义timeout
接口,IsTimeout
方法判断错误的行为。
type timeout interface {
Timeout() bool
}
func IsTimeout(err error) bool {
t, ok := err.(timeout)
return ok && t.Timeout()
}
当相关error
实现Timeout
方法后即可判断其存在超时行为。
if IsTimeout(err) {
// time out
}
这里关键,可以在不到入定义错误的包或者不了解底层类型时判断错误的性质。
4、Wrap Error
Wrap Error
可以将最底层的错误保存的同时添加更多上下文信息,需要判断错误性质时可以获取底层错误。
Wrap Error
目前有两种方式,一种是标准库的errors
包,另外一种就是第三方包github.com/pkg/errors
(下称pkg/errors
),在目前go 1.x
版本下,pkg/errors
相比于errors
包可以记录堆栈信息,方便调试。
package main
import (
"errors"
"fmt"
pkError "github.com/pkg/errors"
)
func main() {
err := service()
fmt.Printf("casue err: %+v\n", pkError.Cause(err))
fmt.Printf("err: %+v", err)
}
func service() error {
err := biz()
return pkError.WithMessage(err, "this is service")
}
func biz() error {
err := dao()
return pkError.WithMessage(err, "this is biz")
}
func dao() error {
err := errors.New("dao err")
return pkError.Wrap(err, "this is dao")
}
// 打印信息
// casue err: dao err
// err: dao err
// this is dao
// main.dao
// .../main.go:26 main.biz
// .../main.go:20 main.service
// .../main.go:15 main.main
// .../main.go:10 runtime.main
// /usr/local/go/src/runtime/proc.go:225 runtime.goexit
// /usr/local/go/src/runtime/asm_amd64.s:1371
// this is biz
// this is service
这里假设dao
层获取对数据的操作错误后,使用pkg/errors
将错误Wrap
后返回,biz
层获取到dao
层错误使用WithMessage
增加相关上下文,最终传递上层可打印出上下文和堆栈信息,并且可以使用Cause
(或者Unwrap
)获取最底层错误。
当需要比较错误时,可使用Is
和As
方法,如果错误已经被Wrap
,它们会获取最底层错误进行比较。
func Is(err, target error) bool
func As(err, target interface{}) bool
As
相比Is
,As
会在err
的底层错误与target
相等时,将err
赋值给target
。
Wrap Error
小结:Wrap Error
可以保留Sentinal Error
特性的同时携带上下文信息,一定程度上也兼容了Error Type
特性,在日常编码中推荐使用这种方式。
注意事项:
1. 如果函数返回了value,error
,必须判定error
,否则返回的value
不可用,除非你连value
也不关心
2. error
应该只被处理一次(打印日志也算是对error
的一种处理),要么降级处理,要么向上层返回error
。
3. 当与第三方库或者基础库交互时,可以将第一次错误Wrap
(只能Wrap
一次,否则将重复打印堆栈信息)向上返回,最终在顶层打印日志,可以避免日志的重复打印及调试方便。
4.使用哪种error
的处理方式,最终由使用场景来决定。
本作品采用《CC 协议》,转载必须注明作者和本文链接