关于后端:先睹为快Go2-Error-的挣扎之路

57次阅读

共计 5734 个字符,预计需要花费 15 分钟才能阅读完成。

若有任何问题或倡议,欢送及时交换和碰撞。我的公众号是【脑子进煎鱼了】,GitHub 地址:https://github.com/eddycjy。

大家好,我是煎鱼。

自从 Go 语言在国内炽热以来,除去泛型,其次最具槽点的就是 Go 对谬误的解决形式,一句经典的 if err != nil 暗号就能认出你是一个 Go 语言爱好者。

天然,大家对 Go error 的关注度更是低落,Go team 也是,因而在 Go 2 Draft Designs 中正式提到了 error handling(错误处理)的相干草案,心愿可能在将来正式的解决这个问题。

在明天这篇文章中,咱们将一起跟踪 Go2 error,看看他是怎么“挣扎”的,能不能破局?

为什么要吐槽 Go1

要吐槽 Go1 error,就得先晓得为什么大家到底是在喷 Error 哪里解决的不好。在 Go 语言中,error 其实实质上只是个 Error 的 interface

type error interface {Error() string
}

理论的利用场景如下:

func main() {x, err := foo()
    if err != nil {// handle error}
}

单纯的看这个例子仿佛没什么问题,但工程大了后呢?显然 if err != nil 的逻辑是会沉积在工程代码中,Go 代码里的 if err != nil 甚至会达到工程代码量的 30% 以上:

func main() {x, err := foo()
    if err != nil {// handle error}
    y, err := foo()
    if err != nil {// handle error}
    z, err := foo()
    if err != nil {// handle error}
    s, err := foo()
    if err != nil {// handle error}
}

暴力的比照一下,就发现四行函数调用,十二行谬误,还要苦练且精通 IDE 的疾速折叠性能,还是比拟麻烦的。

另外既然是错误处理,那必定不单单是一个 return err 了。在工程实际中,我的项目代码都是层层嵌套的,如果间接写成:

if err != nil {return err}

在理论工程中必定是不行。你怎么晓得具体是哪里抛出来的错误信息,理论出错时只能瞎猜。大家又想出了 PlanB,那就是加各种形容信息:

if err != nil {logger.Errorf("煎鱼报错 err:%v", err)
    return err
}

尽管看上去人模人样的,在理论出错时,也会遇到新的问题,因为你要去查这个谬误是从哪里抛出来的,单纯几句谬误形容是难以定位的。这时候就会倒退成 到处打谬误日志

func main() {err := bar()
    if err != nil {logger.Errorf("bar err:%v", err)
    }
    ...
}

func bar() error {_, err := foo()
    if err != nil {logger.Errorf("foo err:%v", err)
        return err
    }

    return nil
}

func foo() ([]byte, error) {s, err := json.Marshal("hello world.")
    if err != nil {logger.Errorf("json.Marshal err:%v", err)
        return nil, err
    }

    return s, nil
}

尽管到处打了日志,就会变成谬误日志十分多,一旦出问题,人肉可能短时间内辨认不进去。且最常见的就是到 IDE 上 ctrl + f 搜寻是在哪出错,同时在咱们经常会自定义一些谬误类型,而在 Go 则须要各种判断和解决:

if err := dec.Decode(&val); err != nil {if serr, ok := err.(*json.SyntaxError); ok {...}
    return err
}

首先你得判断不等于 nil,还得对自定义的谬误类型进行断言,整体来讲比拟繁琐。

汇总来讲,Go1 错误处理的问题至多有:

  • 在工程实际中,if err != nil 写的烦,代码中一大堆错误处理的判断,占了相当的比例,不够优雅。
  • 在排查问题时,Go 的 err 并没有其余堆栈信息,只能本人减少形容信息,层层叠加,打一大堆日志,排查很麻烦。
  • 在验证和测试谬误时,要自定义谬误(各种判断和断言)或者被迫用字符串校验。

Go1.13 的挽尊

在 2019 年 09 月,Go1.13 正式公布。其中两个比拟大的两个关注点别离是包依赖治理 Go modules 的转正,以及错误处理 errors 规范库的改良:

在本次改良中,errors 规范库引入了 Wrapping Error 的概念,并减少了 Is/As/Unwarp 三个办法,用于对所返回的谬误进行二次解决和辨认。同时也是将 Go2 error 预布局中没有毁坏 Go1 兼容性的相干性能提前实现了。

简略来讲,Go1.13 后 Go 的 error 就能够嵌套了,并提供了三个配套的办法。例子:

func main() {e := errors.New("脑子进煎鱼了")
    w := fmt.Errorf("快抓住:%w", e)
    fmt.Println(w)
    fmt.Println(errors.Unwrap(w))
}

输入后果:

$ go run main.go
快抓住:脑子进煎鱼了
脑子进煎鱼了

在上述代码中,变量 w 就是一个嵌套一层的 error。最外层是“快抓住:”,此处调用 %w 意味着 Wrapping Error 的嵌套生成。因而最终输入了“快抓住:脑子进煎鱼了”。

须要留神的是,Go 并没有提供 Warp 办法,而是间接扩大了 fmt.Errorf 办法。而下方的输入因为间接调用了 errors.Unwarp 办法,因而将“取”出一层嵌套,最终间接输入“脑子进煎鱼了”。

对 Wrapping Error 有了根本了解后,咱们简略介绍一下三个配套办法:

func Is(err, target error) bool
func As(err error, target interface{}) bool
func Unwrap(err error) error

errors.Is

办法签名:

func Is(err, target error) bool

办法例子:

func main() {if _, err := os.Open("non-existing"); err != nil {if errors.Is(err, os.ErrNotExist) {fmt.Println("file does not exist")
        } else {fmt.Println(err)
        }
    }

}

errors.Is 办法的作用是判断所传入的 err 和 target 是否同一类型,如果是则返回 true。

errors.As

办法签名:

func As(err error, target interface{}) bool

办法例子:

func main() {if _, err := os.Open("non-existing"); err != nil {
        var pathError *os.PathError
        if errors.As(err, &pathError) {fmt.Println("Failed at path:", pathError.Path)
        } else {fmt.Println(err)
        }
    }

}

errors.As 办法的作用是从 err 谬误链中辨认和 target 雷同的类型,如果能够赋值,则返回 true。

errors.Unwarp

办法签名:

func Unwrap(err error) error

办法例子:

func main() {e := errors.New("脑子进煎鱼了")
    w := fmt.Errorf("快抓住:%w", e)
    fmt.Println(w)
    fmt.Println(errors.Unwrap(w))
}

该办法的作用是将嵌套的 error 解析进去,若存在多级嵌套则须要调用屡次 Unwarp 办法。

民间自救 pkg/errors

Go1 的 error 解决诚然存在许多问题,因而在 Go1.13 前,早已有“民间”发现没有上下文调试信息在理论工程利用中存在重大的体感问题。因而 github.com/pkg/errors 在 2016 年诞生了,目前该库也曾经受到了极大的关注。

官网例子如下:

type stackTracer interface {StackTrace() errors.StackTrace
}

err, ok := errors.Cause(fn()).(stackTracer)
if !ok {panic("oops, err does not implement stackTracer")
}

st := err.StackTrace()
fmt.Printf("%+v", st[0:2]) // top two frames

// Example output:
// github.com/pkg/errors_test.fn
//    /home/dfc/src/github.com/pkg/errors/example_test.go:47
// github.com/pkg/errors_test.Example_stackTrace
//    /home/dfc/src/github.com/pkg/errors/example_test.go:127

简略来讲,就是对 Go1 error 的上下文解决进行了优化和解决,例如类型断言、调用堆栈等。若有趣味的小伙伴能够自行到 github.com/pkg/errors 进行学习。

另外你可能会发现 Go1.13 新增的 Wrapping Error 体系与 pkg/errors 有些相像。你并没有领会错,Go team 接收了相干的意见,对 Go1 进行了调整,但调用堆栈这块因综合起因临时没有纳入。

Go2 error 要解决什么问题

在后面咱们聊了 Go1 error 的许多问题,以及 Go1.13 和 pkg/errors 的自救和交融。你可能会纳闷,那 …Go2 error 还有出场的机会吗?即便 Go1 做了这些事件,Go1 error 还有问题吗?

并没有解决,if err != nil 仍旧一把梭,目前社区声音仍然认为 Go 语言的错误处理要改良。

Go2 error proposal

在 2018 年 8 月,官网正式颁布了 Go 2 Draft Designs,其中蕴含泛型和错误处理机制改良的初步草案:

注:Go1.13 正式将一些不毁坏 Go1 兼容性的 Error 个性退出到了 main branch,也就是后面提到的 Wrapping Error。

错误处理(Error Handling)

第一个要解决的问题就是大量 if err != nil 的问题,针对此提出了 Go2 error handling 的草案设计。

简略例子:

if err != nil {return err}

优化后的计划如下:

func CopyFile(src, dst string) error {
    handle err {return fmt.Errorf("copy %s %s: %v", src, dst, err)
    }

    r := check os.Open(src)
    defer r.Close()

    w := check os.Create(dst)
    handle err {w.Close()
        os.Remove(dst) // (only if a check fails)
    }

    check io.Copy(w, r)
    check w.Close()
    return nil
}

主函数:

func main() {
    handle err {log.Fatal(err)
    }

    hex := check ioutil.ReadAll(os.Stdin)
    data := check parseHexdump(string(hex))
    os.Stdout.Write(data)
}

该提案引入了两种新的语法模式,首先是 check 关键字,其能够选中一个表达式 check f(x, y, z)check err,其将会标识这是一个显式的谬误查看。

其次引入了 handle 关键字,用于定义谬误处理程序流转,逐级上抛,依此类推,直到处理程序执行 return 语句,才正式完结。

谬误值打印(Error Printing)

第二个要解决的问题是谬误值(Error Values)、谬误查看(Error Inspection)的问题,其引申出谬误值打印(Error Printing)的问题,也能够认为是谬误格式化的不便当。

官网针对此提出了提出了 Error Values 和 Error Printing 的草案设计。

简略例子如下:

if err != nil {return fmt.Errorf("write users database: %v", err)
}

优化后的计划如下:

package errors

type Wrapper interface {Unwrap() error
}

func Is(err, target error) bool
func As(type E)(err error) (e E, ok bool)

该提案减少了谬误链的 Wrapping Error 概念,并同时减少 errors.Iserrors.As 的办法,与后面说到的 Go1.13 的改良统一,不再赘述。

须要注意的是,Go1.13 并没有实现 %+v 输入调用堆栈的需要,因为此举会毁坏 Go1 兼容性和产生一些性能问题,大略会在 Go2 退出。

try-catch 不香吗

社区中另外一股声音就是直指 Go 语言反人类不必 try-catch 的机制,在社区内也产生了大量的探讨,具体能够看看相干的提案 Proposal: A built-in Go error check function, “try”。

目前该提案已被回绝,具体可参见 go/issues/32437#issuecomment-512035919 和 Why does Go not have exceptions。

总结

在这篇文章中,咱们介绍了目前 Go1 Error 的现状,概括了大家对 Go 语言错误处理的常见问题和意见。同时还介绍了在这几年间,Go team 针对 Go2、Go1.13 Error 的继续优化和摸索。

如果是你,你会怎么去优化目前 Go 语言的错误处理机制呢,当初 Go2 error proposal 你又是否认可?

参考

  • Golang error 的解围
  • 为什么 Go 语言的 Error Handling 是一个败笔
  • Go 语言 (golang) 新公布的 1.13 中的 Error Wrapping 深度剖析

我的公众号

分享 Go 语言、微服务架构和奇怪的零碎设计,欢送大家关注我的公众号和我进行交换和沟通。

最好的关系是相互成就 ,各位的 点赞 就是煎鱼创作的最大能源,感激反对。

正文完
 0