带你熟悉golang error相关的知识.
0.引言
在这篇文章中,我会带你熟悉golang error相关的知识,大概内容如下:
- golang error和其他语言的对比,好处与弊端
- golang 中各种类型的错误以及推荐的错误类型
- 如何处理错误
- 对go1.13的错误包进行解析
- 对golang error相关知识进行总结、梳理
- 思考题
如果本文中有任何的错误、或者你有一些好的想法都可以及时的提出,希望我们共同进步。
注:本文中的所有代码都在:https://github.com/driftingbo...
1.Error vs Exception
1.1 Error本质
- Error本质上是一个接口
type error interface{
Error() string
}- 经常使用errors.New()来返回一个error对象
例如标准库中的error定义, 通过bufio 前缀带上上下文信息
var (
ErrInvalidUnreadByte = errors.New("bufio: invalid use of UnreadByte")
ErrInvalidUnreadRune = errors.New("bufio: invalid use of UnreadRune")
ErrBufferFull = errors.New("bufio: buffer full")
ErrNegativeCount = errors.New("bufio: negative count")
)- errors.New()是返回的error对象的指针
为了防止在error比较时,因为error内部内容定义相同导致两个不同类型的error误判相等
1.2 Error和Exception的区别
各语言演进:
- C: 一般传入指针,通过返回的int值判断成功还是失败
- C++: 无法知道抛出的什么异常,是否抛出异常(只能通过文档)
- JAVA: 需要抛出异常则方法的所有者必须声明,调用者也必须处理。处理方式、轻重程度都由调用者区分。
- GO: 不引入exception,采用多参数返回,一般最后一个返回值都是error
javagopanicjavaexceptionerrorpanicpanic recover1)性能问题,频繁 panic recover 性能不好
2)容易导致程序异常退出,只要有一个地方没有处理到就会导致程序进程整个退出
3)不可控,一旦 panic 就将处理逻辑移交给了外部,我们并不能预设外部包一定会进行处理什么时候使用 panic 呢?
对于真正意外的情况,那些表示不可恢复的程序错误,例如索引越界、不可恢复的环境问题、栈溢出,我们才使用 panic
使用error代替exception的好处:
- 简单
- 考虑失败不是成功
- 没有隐藏的控制流
- error are value
2.Error Type 🌟
2.1 Sentinel Error
ErrInvalidUnreadByte = errors.New("bufio: invalid use of UnreadByte")预定义的错误,缺点:
- 不灵活,调用方使用==去比较错误值是否相等;一旦出现fmt.Errorf这种携带上下文信息的error,会破坏相等性检查
- 成为你的公共api;比如io.reader,io.copy这类函数都需要去判断错误类型是否是io.eof,但这并不是一个错误。
- 创建了两个包之间的依赖
2.2 Error Types
Error types 是实现了 error 接口的自定义类型。例如 MyError 类型记录了文件和行号以展示发生了什么:
// PathError records an error and the operation and file path that caused it.
type PathError struct {
Op string
Path string
Err error
}
func (e *PathError) Error() string { return e.Op + " " + e.Path + ": " + e.Err.Error() }
func (e *PathError) Unwrap() error { return e.Err }Error types 优点:
- 携带更多的上下文
Error types 缺点:
- 调用者要使用类型断言和类型 switch,就要让自定义的 error 变为 public。这种模型会导致和调用者产生强耦合,从而导致 API 变得脆弱。
- 共享 error values 许多相同的问题。
2.3 Opaque Error
errors.Cause(err, sql.ErrNoRows)xerrors.Is(err, sql.ErrNoRows)sql.ErrNoRows如果只是利用库代码进行业务开发, 包装后作判断的作法可以被理解和接受的。
而对于API的定义者来说, 这个问题就变得需要格外重视,我们需要
不透明的错误处理。它的优势在于:减少代码之间耦合,调用者只需关心成功还是失败,无需关心错误内部
// 只需返回错误而不假设其内容
func fn()error{
x, err := bar.Foo()
if err != nil {
return err
}
// to do something
}说白了就是不通过err来判断各种情况,作为调用者只关心是成功还是失败(err是否为nil)
Assert errors for behaviour, not type
在少数情况下,只有这种二分的处理方法是不够(只有成功、失败两种状态)。
// An Error represents a network error.
type Error interface {
error
Timeout() bool // Is the error a timeout?
Temporary() bool // Is the error temporary?
}
if nerr, ok := err.(net.Error); ok && nerr.Temporary() {
// to do something
}type temporary interface {
Temporary() bool
}
// 在不导入包的情况下可以直接使用err相关行为
func IsTemporary(err error) bool {
te, ok := err.(temporary)
return ok && te.Temporary()
}IsTemporarytemporaryTemporaryTemporarytruetrueerr我建议,
尽量避免Sentinel Error、error types,去使用Opaque Error,至少在库文件、公共api中.
3.Handing Error Gracefully 🌟
3.1 Try to eliminate errors handle
1)在不破坏程序正确性和可读性的前提下,尽量减少error处理
例一
| Bad | Good |
|---|---|
| func AuthRequest() error{<br/> if err:= auth();err!=nil{<br/> return err<br/> }<br/> return nil<br/>} | func AuthRequest(r, *Request) error{<br/> return auth(r.User)<br/>} |
例二
if error != nil {...}我们先看一个令人崩溃的代码。
func parse(r io.Reader) (*Point, error) {
var p Point
if err := binary.Read(r, binary.BigEndian, &p.Longitude); err != nil {
return nil, err
}
if err := binary.Read(r, binary.BigEndian, &p.Latitude); err != nil {
return nil, err
}
if err := binary.Read(r, binary.BigEndian, &p.Distance); err != nil {
return nil, err
}
if err := binary.Read(r, binary.BigEndian, &p.ElevationGain); err != nil {
return nil, err
}
if err := binary.Read(r, binary.BigEndian, &p.ElevationLoss); err != nil {
return nil, err
}
}binary.Read()bufio.Scannerscanner := bufio.NewScanner(input)
for scanner.Scan() {
token := scanner.Text()
// process token
}
if err := scanner.Err(); err != nil {
// process the error
}错误会保存到Scanner中,只进行最后一次的判断。应用这个思路,优化代码如下
type Reader struct {
r io.Reader
err error
}
func (r *Reader) read(data interface{}) {
if r.err == nil {
r.err = binary.Read(r.r, binary.BigEndian, data)
}
}
func parse(input io.Reader) (*Point, error) {
var p Point
r := Reader{r: input}
r.read(&p.Longitude)
r.read(&p.Latitude)
r.read(&p.Distance)
r.read(&p.ElevationGain)
r.read(&p.ElevationLoss)
if r.err != nil {
return nil, r.err
}
return &p, nil
}if err != nil3.2 Annotating errors
errorerror注释error注释fmt.ErrorfError() pkg/errors".Wraperror注释- 是否需要被调用者捕获、处理
func Open(filePath string) error {
_, err := createfile(filePath)
return fmt.Errorf(“createfile: %s“, err)
}fmt.Errorfpkg/errors // file package
var FileNotExsist = “file path not exsist: %s“
func Open(filePath string) error {
_, err := createfile(filePath)
return errors.warp(err, "createfile fail: ")
}这里简单介绍一下这个包github.com/pkg/errors,使用它去打包下游信息几乎已经是一个golang中的标准做法了。
它的使用非常简单,如果我们要新生成一个错误,可以使用New函数,生成的错误,自带调用堆栈信息。
// fundamental is an error that has a message and a stack, but no caller.
type fundamental struct {
msg string
*stack
}
func New(message string) error {
return &fundamental{
msg: message,
stack: callers(),
}
}这里的fundamental对象也是实现了 golang 内建 interface error的 Error 方法.
如果有一个现成的error,我们需要对他进行再次包装处理,这时候有三个函数可以选择。
//只附加新的信息, 一般用于在业务代码中替换 fmt.Errorf()
func WithMessage(err error, message string) error
//只附加调用堆栈信息,一般用于包装对第三方代码(标准库或第三方库)的调用。
func WithStack(err error) error
//同时附加堆栈和信息,一般用于包装对第三方代码(标准库或第三方库)的调用。
func Wrap(err error, message string) error其实上面的包装,很类似于Java的异常包装,被包装的error,就是这个错误的根本原因。所以这个错误处理库为我们提供了Cause函数让我们可以获得最原始的 error 对象。
func Cause(err error) error {
type causer interface {
Cause() error
}
for err != nil {
cause, ok := err.(causer)
if !ok {
break
}
err = cause.Cause()
}
return err
}使用for循环一直找到最根本(最底层)的那个error。
以上的错误我们都包装好了,也收集好了,那么怎么把他们里面存储的堆栈、错误原因等这些信息打印出来呢?其实,这个错误处理库的错误类型,都实现了Formatter接口,我们可以通过fmt.Printf函数输出对应的错误信息。
- %s,%v //功能一样,输出错误信息,不包含堆栈
- %q //输出的错误信息带引号,不包含堆栈
- %+v //输出错误信息和堆栈
⚠️ 不要多次包装错误,堆栈信息会重复。
如果多次使用 WithStack(err),会将 stack 打印多遍,err 信息可能非常长。 可以人肉去 check 下层有没有使用 WithStack(err),如果下层用了上层就不用。但这样会增加心智负担,容易出错。 我们可以在调用是使用一个 wrap 函数,判断一下是否已经执行 WithStack(err)。 但是 github.com/pkg/errors 自定义的 error 类型 withStack 是私有类型,如何去判断是否已经执行 WithStack(err) 呢? 好在 StackTrace 不是私有类型,所以我们可以使用 interface 的一个小技巧,自己定义一个 interface,如果拥有 StackTrace() 方法则不再执行 WithStack(err)。 像这样:
type stackTracer interface {
StackTrace() errors2.StackTrace
}
// once
func WithStack(err error) error {
_, ok := err.(stackTracer)
if ok {
return err
}
return errors2.WithStack(err)
}3.3 Only handle errors once
Handling an error means inspecting the error value, and making a decision.
dave.cheney如果你做出的决定少于一个,那么就是你没有检查错误、忽略了错误,如下:
func Write(w io.Writer, buf []byte) {
w.Write(buf)
}w.Write(buf)的error被丢弃了
如果针对一个问题做出的决定多于一个,也是有问题的,如下:
func Write(w io.Writer, buf []byte) error {
_, err := w.Write(buf)
if err != nil {
// annotated error goes to log file
log.Println("unable to write:", err)
// unannotated error returned to caller
return err
}
return nil
}在上面的例子中
我们既记录错误日志,又错误返回给调用者,返回的错误可能会被层层记录,一直到最上层。
最后我们会得到一堆重复的日志信息,但在最上层却只能拿到一个最原始的错误。
error注释func Write(w io.Write, buf []byte) error {
_, err := w.Write(buf)
return errors.Wrap(err, "write failed")
}4.Golang1.13 error
结合社区反馈,Go 团队完成了在 Go 2 中简化错误处理的提案。 Go核心团队成员 Russ Cox 在xerrors中部分实现了提案中的内容。它用与 github.com/pkg/errors相似的思路解决同一问题, 引入了一个新的 fmt 格式化动词: %w,使用 Is 进行判断。:
import (
"database/sql"
"fmt"
"golang.org/x/xerrors"
)
func bar() error {
if err := foo(); err != nil {
return xerrors.Errorf("bar failed: %w", foo())
}
return nil
}
func foo() error {
return xerrors.Errorf("foo failed: %w", sql.ErrNoRows)
}
func main() {
err := bar()
if xerrors.Is(err, sql.ErrNoRows) {
fmt.Printf("data not found, %v\n", err)
fmt.Printf("%+v\n", err)
return
}
if err != nil {
// unknown error
}
}
/* Outputs:data not found, bar failed: foo failed: sql: no rows in result set
bar failed:
main.bar
/usr/four/main.go:12
- foo failed:
main.foo
/usr/four/main.go:18
- sql: no rows in result set
*/与 github.com/pkg/errors 相比,它有几点不足:
- 使用 : %w 代替了 Wrap 看似简化, 但失去了编译期检查。 如果没有冒号,或 : %w 不位于于格式化字符串的结尾,或冒号与百分号之间没有空格,包装将失效且不报错
- 更严重的是, 调用 xerrors.Errorf 之前需要对参数进行nil判断。 这实际完全没有简化开发者的工作
到了 Go 1.13 ,xerrors 的部分功能被整合进了标准库。 它继承了 xerrors的全部缺点, 并额外贡献了一项:不支持调用栈信息输出.
根据官方的说法, 此功能没有明确时间表。因此其实用性远低于 github.com/pkg/errors
因此目前没有使用它的必要
5.go 2 inspection
sum typeprotoBufferoneofvar int|error result那么这个result要么是int,要么是error类型;
这也解决了go中没有泛型代理的部分问题;
6.总结
最后,我来帮你梳理一下本文的重点,也就是目前需要掌握的
// TODO 图片
上图是分别在业务代码中和库文件、api代码中使用error的建议
panic
func Go(f func()){
go func(){
defer func(){
if err := recover(); err != nil {
log.Printf("panic: %+v", err)
}
}()
f()
}()
}func (u *usecese) usecase1() error {
money := u.repo.getMoney(uid)
if money < 10 {
errors.Errorf("用户余额不足, uid: %d, money: %d", uid, money)
}
// 其他逻辑
return nil}
func (u *usecese) usecase2() error {
name, err := u.repo.getUserName(uid)
if err != nil {
return errors.WithMessage(err, "其他附加信息")
}
// 其他逻辑
return nil
}func f() error {
err := A()
if errors.Is(err, io.EOF){
return nil
}
// 其他逻辑
return nil
}func f() error {
err := A()
if errA := new(errorA) && errors.As(err, &errA){
// ...
}
// 其他逻辑
return nil
}最近我也是在设计、重构公司微服务错误、日志相关的基础设施,如果有时间,我也会写一下实践的文章。
7.
- 我们在数据库操作的时候,比如 dao 层中当遇到一个 sql.ErrNoRows 的时候,是否应该 Wrap 这个 error,抛给上层。为什么,应该怎么做请写出代码?
- 为什么不允许处处使用 errors.Wrap ?
- errors.wrap/ WithMessage 有何作用,为什么不用标准库的 fmt.Errorf("%w")
reference
本文由mdnice多平台发布