当前位置:   article > 正文

聊聊Golang饱受争议的error

golang error

大家好,我是阳哥。

这期内容和大家聊聊golang中饱受争议的error,希望对大家有帮助。

也欢迎大家订阅我的 # Go语言学习专栏 ,已经有200多位朋友订阅。

一、error是什么?

在C中,返回错误通过errno.h中的错误代码来表示,比如0代表No error,也就是没有错误;2代表No such file or directory,也就是找不到指定路径的文件或文件夹;5代表Input/Output error,表示输入或输出出现了错误...

而在我们最爱的Golang中,有这样一个饱受争议的error类型,它不是一个整数,而是一个接口。

  1. package main
  2. import (
  3.     "errors"
  4.     "fmt"
  5. )
  6. type name struct {
  7.     error string
  8. }
  9. func (n *name) Error() string {
  10.     return fmt.Sprintf("%s : ...", n.error)
  11. }
  12. func main() {
  13.     
  14.     err := judge(11)
  15.     //err := judge(1)
  16.     //err := judge(6)
  17.     fmt.Println(err)
  18. }
  19. func judge(num int) error {
  20.     if num > 5 && num < 10 {
  21.         return errors.New("这个数字大于5了..")
  22.     }
  23.     if num >= 10 {
  24.         return fmt.Errorf("%d大于或等于10了...", num)
  25.     }
  26.     return &name{error"hello"}
  27. }

这是三种可以作为error返回值的方式。errors.New()创建出来的error类型其实是errorString结构体。

  1. // src/errors/errors.go
  2. // New returns an error that formats as the given text.
  3. // Each call to New returns a distinct error value even if the text is identical.
  4. func New(text string) error {
  5.  return &errorString{text}
  6. }
  7. // errorString is a trivial implementation of error.
  8. type errorString struct {
  9.  s string
  10. }
  11. func (e *errorString) Error() string {
  12.  return e.s
  13. }

所以我们创建的结构体name其实和errors.New()底层的形式是基本一样的。

而使用的fmt.Errorf其实是先将字符串格式化然后再调用errors.New()。

二、error引人争论的点在哪?

在Go中,设计者从语言层面要求人们需要明确地处理遇到的错误,但是因此导致的问题也十分明显,使用Go语言编写的代码中err会到处都是,不过优秀的IDE——Goland能够解决这个问题,使用Goland能够将err!=nil这段判断和处理压缩,不再干扰代码的阅读。

我本人是不太喜欢Java的try-catch机制,可能是不太会用,Go语言官方提到try-catch会让代码变得比较混乱,很多程序员会胡乱catch异常,导致错误处理比较冗长。

而Go语言通过多返回值机制,让返回错误变得很简单,并且提供panic和error两种机制,感觉这种机制更有优势,也看起来更简洁。

煎鱼大佬之前有博客谈到了Go社区中关于Go错误处理的新议题,大家想了解的可以看看:

  • Go 错误处理新思路?用左侧函数和表达式:https://juejin.cn/post/7102268049213882398

  • Go try 新提案靠谱吗?想简化错误处理了 https://juejin.cn/post/7157931922008571940

其实之前Go社区中出现过多种关于错误处理的新议题,但是都没有被采纳...

三、如何优雅的处理错误
1. 避免处理“哨兵错误”,即Sentinel errors

比如为了判断err == io.EOF就得引入io包,这是标准库的包还能接受,如果是第三方库的包,并且使用“哨兵错误”,很容易导致循环引用的问题。

2. 避免使用error类型

虽然这种错误比“哨兵错误”要好,它可以捕获更多关于错误的上下文信息,比如出错的行数等其他字段信息。但是又不可避免地在定义错误和使用错误的包之间形成依赖关系,又容易导致循环引用的问题。

3. 使用不透明的“黑盒错误”
  1. func f() error{
  2.     sentence,err := say.Hello()
  3.     if err != nil{
  4.         return err
  5.     }
  6.     // ...
  7. }

上面这种写法是不是我们经常会用到?这种情况下,我们只需要判断err是否为空,不为空,代表有错误,就直接返回错误,否则就继续执行后面的流程。

作为程序执行者,你没有能力看到程序错误的内部信息,只能知道程序有错或者没有错误。这种错误处理作为一种调试辅助手段还是不错的。

4. 使用Warp和Cause

第三方库github.com/pkg/errors可以输出错误堆栈,并且使用起来很简单,大家可以了解一下。

  1. // Wrap annotates cause with a message.
  2. func Wrap(cause error, message string) error
  3. // Cause unwraps an annotated error.
  4. func Cause(err error) error

下面来介绍Wrap和Cause的使用样例:

  1. func ReadFile(path string) ([]byteerror) {
  2.         f, err := os.Open(path)
  3.         if err != nil {
  4.                 return nil, errors.Wrap(err, "open failed")
  5.         }
  6.         defer f.Close()
  7.         buf, err := ioutil.ReadAll(f)
  8.         if err != nil {
  9.                 return nil, errors.Wrap(err, "read failed")
  10.         }
  11.         return buf, nil
  12. }
  13. func ReadConfig() ([]byteerror) {
  14.         home := os.Getenv("HOME")
  15.         config, err := ReadFile(filepath.Join(home, ".settings.xml"))
  16.         return config, errors.Wrap(err, "could not read config")
  17. }
  18. func main() {
  19.         _, err := ReadConfig()
  20.         if err != nil {
  21.                 fmt.Println(err)
  22.                 os.Exit(1)
  23.         }
  24. }

如果ReadConfig()执行失败,就会得到下面这一行十分美观的报错:

could not read config: open failed: open /Users/dfc/.settings.xml: no such file or directory

而如果用fmt.Printf和%+v格式来输出就能看到更清晰、更有层次的错误堆栈:

  1. func main() {
  2.         _, err := ReadConfig()
  3.         if err != nil {
  4.                 fmt.Printf("%+v",err)
  5.                 os.Exit(1)
  6.         }
  7. }
6ddf6dc7ce88fa8b16f3d2f9750244a2.png

然后我们再来看Cause的使用。

  1. type temporary interface{
  2.     Temporary() bool
  3. }
  4. // IsTemporary returns true if err is temporary.
  5. func IsTemporary(err error) bool {
  6.         te, ok := errors.Cause(err).(temporary)
  7.         return ok && te.Temporary()
  8. }

当需要检查一个错误与一个特定的值或类型时。比如此处,先用Cause取出错误,做断言,最后调用Temporary(),如果断言失败,ok就会是false,就不会调用右边的Temporary()去执行。

如果 && 运算符左侧的子表达式为 false,则不会检查右侧的表达式。因为只要有一个子表达式为 false,则整个表达式都为 false,所以再检查剩余的表达式会浪费 CPU 时间。这被称为短路评估。

一起学习

26ba822d9bf1b8f7f50f84a5bc8bd0dd.png

欢迎学编程的朋友们加入阳哥的 程序员升职加薪知识星球 ,阳哥会 1 对 1 解决你的问题,带你做开源项目、为你定制学习计划和求职指导,还能获取海量编程学习资源,和上万名学编程的朋友共享知识、交流进步。

往期推荐

我的学习小圈子

粉丝福利:参与活动给大家发红包

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/article/detail/42816
推荐阅读
相关标签