标签 错误处理 下的文章

要么返回错误值,要么输出日志,别两样都做

本文永久链接 – https://tonybai.com/2024/04/14/either-return-error-or-log-them-do-not-do-both

1. 缘起

这周,一个产品团队内进行Go代码评审时,得到了一个结论:所有的if err != nil的地方都应该输出错误日志。然而,这种做法并不是最佳实践,它存在一些问题。

首先,打印过多的错误日志会导致日志文件变得冗长和难以阅读。其次,重复的错误信息会增加冗余。此外,每一层都打印错误日志,一旦错误信息设计不当,可能会导致上下文信息的丢失。

让我们来看一个示例,说明为什么同时输出错误日志和返回错误值会导致问题。假设我们有一个五层的Go函数调用栈,其中最底层的函数level4Function出现了一个错误:

package main

import (
    "fmt"
    "log"
)

func main() {
    if err := topFunction(); err != nil {
        log.Printf("Error: %v", err)
    }
}

func topFunction() error {
    err := level1Function()
    if err != nil {
        log.Printf("topFunction: %v", err)
        return err
    }
    return nil
}

func level1Function() error {
    err := level2Function()
    if err != nil {
        log.Printf("level1Function: %v", err)
        return err
    }
    return nil
}

func level2Function() error {
    err := level3Function()
    if err != nil {
        log.Printf("level2Function: %v", err)
        return err
    }
    return nil
}

func level3Function() error {
    err := level4Function()
    if err != nil {
        log.Printf("level3Function: %v", err)
        return err
    }
    return nil
}

func level4Function() error {
    err := fmt.Errorf("something went wrong")
    log.Printf("level4Function: %v", err)
    return err
}

在这个示例中,我们在每个函数中都输出错误日志并返回错误值。我们运行一下这个程序:

$go run main.go
2024/04/14 23:10:05 level4Function: something went wrong
2024/04/14 23:10:05 level3Function: something went wrong
2024/04/14 23:10:05 level2Function: something went wrong
2024/04/14 23:10:05 level1Function: something went wrong
2024/04/14 23:10:05 topFunction: something went wrong
2024/04/14 23:10:05 Error: something went wrong

当我们运行程序时,日志文件会出现重复的错误信息,并且上下文信息不易于进行链式追踪,因为每个函数只打印了特定错误的信息,而没有提供之前错误的上下文。

2. 好的实践技巧

为了解决上述问题,我们需要采用一种更好的实践方法。面向调用层次较深的函数调用栈,我们应该只在最顶层的函数中输出错误日志,而在下层函数中返回错误值。但是,我们需要精心构造错误值,以形成基于wrapped error的错误链。

让我们修改示例代码,按照最佳实践进行错误处理:

package main

import (
    "fmt"
    "log"
)

func main() {
    if err := topFunction(); err != nil {
        log.Printf("Error: %v", err)
    }
}

func topFunction() error {
    err := level1Function()
    if err != nil {
        return fmt.Errorf("topFunction: %w", err)
    }
    return nil
}

func level1Function() error {
    err := level2Function()
    if err != nil {
        return fmt.Errorf("level1Function: %w", err)
    }
    return nil
}

func level2Function() error {
    err := level3Function()
    if err != nil {
        return fmt.Errorf("level2Function: %w", err)
    }
    return nil
}

func level3Function() error {
    err := level4Function()
    if err != nil {
        return fmt.Errorf("level3Function: %w", err)
    }
    return nil
}

func level4Function() error {
    err := fmt.Errorf("something went wrong")
    return fmt.Errorf("level4Function: %w", err)
}

在这个修改后的示例中,我们在每个函数中使用fmt.Errorf+%w将错误包装为一个wrapped error,并将前一层的错误作为参数传递。通过这种方式,我们构建了一个错误链,其中每个错误都包含了之前发生的错误上下文。在最顶层的main函数中,我们使用日志库输出错误日志,下面是示例程序的运行结果:

2024/04/14 23:12:16 Error: topFunction: level1Function: level2Function: level3Function: level4Function: something went wrong

我们看到:通过这种方法,我们避免了重复的错误日志,并保留了错误的上下文信息,快速定位了根因。当运行修改后的程序时,我们会看到日志文件中只打印了完整的错误链,而不是重复的错误信息。通过调用链和精心设计的错误上下文,我们还可以看到函数调用链,这使得错误的调试和处理变得更加方便和可靠。

关于错误链的使用,大家可以看看我之前撰写的《Go错误处理:错误链使用指南》一文。

3. 小结

在前面的示例中,我们展示了同时输出错误日志和返回错误值的问题,并介绍了如何使用wrapped error来构建错误链。通过合理地处理错误,我们可以提高代码的可读性和可维护性,同时也有助于快速定位和解决问题。

总之,在编写Go代码时,请记住要么返回错误值,要么输出日志,不要两者都做。通过合理地处理错误,我们可以编写出更可靠、更易于调试的代码。


Gopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时,我们也会加强代码质量和最佳实践的分享,包括如何编写简洁、可读、可测试的Go代码。此外,我们还会加强星友之间的交流和互动。欢迎大家踊跃提问,分享心得,讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落,享受coding的快乐! 欢迎大家踊跃加入!

img{512x368}
img{512x368}

img{512x368}
img{512x368}

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com

我的联系方式:

  • 微博(暂不可用):https://weibo.com/bigwhite20xx
  • 微博2:https://weibo.com/u/6484441286
  • 博客:tonybai.com
  • github: https://github.com/bigwhite
  • Gopher Daily归档 – https://github.com/bigwhite/gopherdaily

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

Go错误处理:错误链使用指南

本文永久链接 – https://tonybai.com/2023/05/14/a-guide-of-using-go-error-chain

0. Go错误处理简要回顾

Go是一种非常强调错误处理的编程语言。在Go中,错误被表示为实现了error接口的类型的值,error接口只有一个方法:

type error interface {
    Error() string
}

这个接口的引入使得Go程序可以以一致和符合惯用法的方式进行错误处理。

在所有编程语言中,错误处理的挑战之一都是能提供足够的错误上下文信息,以帮助开发人员诊断问题,同时又可以避免开发人员淹没在不必要的细节中。在Go中,这一挑战目前是通过使用错误链(error chain)来解决的。

注:Go官方用户调查结果表明,Go社区对Go错误处理机制改进的期望还是很高的。这对Go核心团队而言,依然是一个不小的挑战。好在Go 1.18泛型落地,随着Go泛型的逐渐成熟,更优雅的错误处理方案有可能会在不远的将来浮出水面。

错误链是一种将一个错误包裹在另一个错误中的技术,以提供关于错误的额外的上下文。当错误通过多层代码传播时,这种技术特别有用,每层代码都会为错误信息添加自己的上下文。

不过,最初Go的错误处理机制是不支持错误链的,Go对错误链的支持和完善是在Go 1.13版本中才开始的事情。

众所周知,在Go中,错误处理通常使用if err != nil的惯用法来完成。当一个函数返回一个错误时,调用代码会检查该错误是否为nil。如果错误不是nil,通常会被打印到日志中或返回给调用者。

例如,看下面这个读取文件的函数:

func readFile(filename string) ([]byte, error) {
    data, err := os.ReadFile(filename)
    if err != nil {
        return nil, err
    }
    return data, nil
}

在这段代码中,os.ReadFile()如果读取文件失败,会返回一个错误。如果发生这种情况,readFile函数会将错误返回给它的调用者。Go的这种基本的错误处理机制简单有效好理解,但它也有自己的局限性。其中一个主要的限制是错误信息可能是模糊的。当一个错误在多层代码中传播时,开发人员可能很难确定错误的真实来源和原因。 我们看一下下面这段代码:

func processFile(filename string) error {
    data, err := readFile(filename)
    if err != nil {
        return fmt.Errorf("can not read file: %s", filename)
    }
    // process the file data...
    return nil
}

在这个例子中,如果processFile因readFile失败而返回一个错误,错误信息将只表明该文件无法被读取,它不会提供任何关于造成错误的原因或错误发生地点的准确信息

Go基本错误处理的另一个约束是在处理错误时,错误的上下文可能会丢失。尤其是当一个错误通过多层代码时,某一层可能会忽略收到的错误信息,而是构造自己的错误信息并返回给调用者,这样最初的错误上下文就会在错误的传递过程中丢失了,这不利于问题的快速诊断。

那么,我们如何解决这些限制呢?下面我们就来探讨一下错误链是如何如何帮助Go开发人员解决这些限制问题的。

1. 错误包装(error wrapping)与错误链

为了解决基本错误处理的局限性,Go在1.13版本中提供了Unwrap接口和fmt.Errorf的%w的格式化动词,用于构建可以包裹(wrap)其他错误的错误以及从一个包裹了其他错误的错误中判断是否有某个指定错误,并从中提取错误信息。

fmt.Errorf是最常用的用于包裹错误的函数,它接收一个现有的错误,并将其包装在一个新的错误中,并可以附着更多的错误上下文信息。

例如,改造一下上面的示例代码:

func processFile(filename string) error {
    data, err := readFile(filename)
    if err != nil {
        return fmt.Errorf("failed to read file: %w", err)
    }
    // process the file data...
    return nil
}

在这段代码中,fmt.Errorf通过%w创建了一个新的错误,新错误包裹(wrap)了原来的错误,并附加了一些错误上下文信息(failed to read file)。这个新的错误可以在调用堆栈中传播并提供更多关于这个错误的上下文。

为了从错误链中检索原始错误,Go在errors包中提供了Is、As和Unwrap()函数。Is和As函数用于判定某个error是否存在于错误链中,Unwrap这个函数返回错误链中的下一个直接错误。

下面是一个完整的例子:

func readFile(filename string) ([]byte, error) {
    data, err := os.ReadFile(filename)
    if err != nil {
        return nil, err
    }
    return data, nil
}

func processFile(filename string) error {
    data, err := readFile(filename)
    if err != nil {
        return fmt.Errorf("failed to read file: %w", err)
    }
    fmt.Println(string(data))
    return nil
}

func main() {
    err := processFile("1.txt")
    if err != nil {
        fmt.Println(err)
        fmt.Println(errors.Is(err, os.ErrNotExist))
        err = errors.Unwrap(err)
        fmt.Println(err)
        err = errors.Unwrap(err)
        fmt.Println(err)
        return
    }
}

运行这个程序(前提:1.txt文件并不存在),结果如下:

$go run demo1.go
failed to read file: open 1.txt: no such file or directory
true
open 1.txt: no such file or directory
no such file or directory

该示例中错误的wrap和unwrap关系如下图:

像这种由错误逐个包裹而形成的链式结构(如下图),我们称之为错误链

接下来,我们再来详细说一下Go错误链的使用。

2. Go中错误链的使用

2.1 如何创建错误链

就像前面提到的,我们通过包裹错误来创建错误链

目前Go标准库中提供的用于wrap error的API有fmt.Errorf和errors.Join。fmt.Errorf最常用,在上面的示例中我们演示过了。errors.Join用于将一组errors wrap为一个error。

fmt.Errorf也支持通过多个%w一次打包多个error,下面是一个完整的例子:

func main() {
    err1 := errors.New("error1")
    err2 := errors.New("error2")
    err3 := errors.New("error3")

    err := fmt.Errorf("wrap multiple error: %w, %w, %w", err1, err2, err3)
    fmt.Println(err)
    e, ok := err.(interface{ Unwrap() []error })
    if !ok {
        fmt.Println("not imple Unwrap []error")
        return
    }
    fmt.Println(e.Unwrap())
}

示例运行输出如下:

wrap multiple error: error1, error2, error3
[error1 error2 error3]

我们看到,通过fmt.Errorf一次wrap的多个error在String化后,是在一行输出的。这点与errors.Join的有所不同。下面是用errors.Join一次打包多个error的示例:

func main() {
    err1 := errors.New("error1")
    err2 := errors.New("error2")
    err3 := errors.New("error3")

    err := errors.Join(err1, err2, err3)
    fmt.Println(err)
    errs, ok := err.(interface{ Unwrap() []error })
    if !ok {
        fmt.Println("not imple Unwrap []error")
        return
    }
    fmt.Println(errs.Unwrap())
}

这个示例输出如下:

$go run demo2.go
error1
error2
error3
[error1 error2 error3]

我们看到,通过errors.Join一次wrap的多个error在String化后,每个错误单独占一行。

如果对上面的输出格式都不满意,那么你还可以自定义Error类型,只要至少实现了String() string和Unwrap() error 或Unwrap() []error即可。

2.2 判定某个错误是否在错误链中

前面提到过errors包提供了Is和As函数来判断某个错误是否在错误链中,对于一次wrap多个error值的情况,errors.Is和As也都按预期可用。

2.3 获取错误链中特定错误的上下文信息

有些时候,我们需要从错误链上获取某个特定错误的上下文信息,通过Go标准库可以至少有两种实现方式:

第一种:通过errors.Unwrap函数来逐一unwrap错误链中的错误。

由于不确定错误链上的error个数以及每个error的特征,这种方式十分适合用来获取root cause error,即错误链中最后面的一个error。下面是一个示例:

func rootCause(err error) error {
    for {
        e, ok := err.(interface{ Unwrap() error })
        if !ok {
            return err
        }
        err = e.Unwrap()
        if err == nil {
            return nil
        }
    }
}

func main() {
    err1 := errors.New("error1")

    err2 := fmt.Errorf("2nd err: %w", err1)
    err3 := fmt.Errorf("3rd err: %w", err2)

    fmt.Println(err3) // 3rd err: 2nd err: error1

    fmt.Println(rootCause(err1)) // error1
    fmt.Println(rootCause(err2)) // error1
    fmt.Println(rootCause(err3)) // error1
}

第二种:通过errors.As函数将error chain中特定类型的error提取出来

error.As函数用于判断某个error是否是特定类型的error,如果是则将那个error提取出来,比如:

type MyError struct {
    err string
}

func (e *MyError) Error() string {
    return e.err
}

func main() {
    err1 := &MyError{"temp error"}
    err2 := fmt.Errorf("2nd err: %w", err1)
    err3 := fmt.Errorf("3rd err: %w", err2)

    fmt.Println(err3)

    var e *MyError
    ok := errors.As(err3, &e)
    if ok {
        fmt.Println(e)
        return
    }
}

在这个示例中,我们通过errors.As将错误链err3中的err1提取到e中,后续就可以使用err1这个特定错误的信息了。

3. 小结

错误链是Go中提供信息丰富的错误信息的一项重要技术。通过用额外的上下文包装错误,你可以提供关于错误的更具体的信息,并帮助开发人员更快地诊断出问题。

不过错误链在使用中有一些事项还是要注意的,比如:避免嵌套错误链。嵌套的错误链会使你的代码难以调试,也难以理解错误的根本原因。

结合错误链,通过给错误添加上下文,创建自定义错误类型,并在适当的抽象层次上处理错误,你可以写出简洁、可读和信息丰富的错误处理代码。


“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于Go语言生态的所有需求!2023年,Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码,关注代码质量并深入理解Go核心技术,并继续加强与星友的互动。欢迎大家加入!

img{512x368}
img{512x368}

img{512x368}
img{512x368}

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily

我的联系方式:

  • 微博(暂不可用):https://weibo.com/bigwhite20xx
  • 微博2:https://weibo.com/u/6484441286
  • 博客:tonybai.com
  • github: https://github.com/bigwhite

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats