经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 程序设计 » Go语言 » 查看文章
Go Error 嵌套到底是怎么实现的?
来源:cnblogs  作者:yongxinz  时间:2022/1/17 11:08:35  对本文有异议

原文链接: Go Error 嵌套到底是怎么实现的?

Go Error 的设计哲学是 「Errors Are Values」。

这句话应该怎么理解呢?翻译起来挺难的。不过从源码的角度来看,好像更容易理解其背后的含义。

Go Error 源码很简单,寥寥几行:

  1. // src/builtin/builtin.go
  2. type error interface {
  3. Error() string
  4. }

error 是一个接口类型,只需要实现 Error() 方法即可。在 Error() 方法中,就可以返回自定义结构体的任意内容。

下面首先说说如何创建 error

创建 Error

创建 error 有两种方式,分别是:

  1. errors.New()
  2. fmt.Errorf()

errors.New()

errors.New() 的使用延续了 Go 的一贯风格,New 一下就可以了。

举一个例子:

  1. package main
  2. import (
  3. "errors"
  4. "fmt"
  5. )
  6. func main() {
  7. err := errors.New("这是 errors.New() 创建的错误")
  8. fmt.Printf("err 错误类型:%T,错误为:%v\n", err, err)
  9. }
  10. /* 输出
  11. err 错误类型:*errors.errorString,错误为:这是 errors.New() 创建的错误
  12. */

这段代码唯一让人困惑的地方可能就是错误类型了,但没关系。只要看一下源码,就瞬间迎刃而解。

源码如下:

  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. }

可以看到,errorString 是一个结构体,实现了 Error() 方法,New 函数直接返回 errorString 指针。

这种用法很简单,但不实用。假如我还想返回程序的上下文信息,它就没辙了。

下面看第二种方式。

fmt.Errorf()

还是先看一个例子:

  1. package main
  2. import (
  3. "database/sql"
  4. "fmt"
  5. )
  6. func foo() error {
  7. return sql.ErrNoRows
  8. }
  9. func bar() error {
  10. return foo()
  11. }
  12. func main() {
  13. err := bar()
  14. if err == sql.ErrNoRows {
  15. fmt.Printf("data not found, %+v\n", err)
  16. return
  17. }
  18. if err != nil {
  19. fmt.Println("Unknown error")
  20. }
  21. }
  22. /* 输出
  23. data not found, sql: no rows in result set
  24. */

这个例子输出了我们想要的结果,但是还不够。

一般情况下,我们会通过使用 fmt.Errorf() 函数,附加上我们想添加的文本信息,使返回内容更明确,处理起来更灵活。

所以,foo() 函数会改成下面这样:

  1. func foo() error {
  2. return fmt.Errorf("foo err, %v", sql.ErrNoRows)
  3. }

这时问题就出现了,经过 fmt.Errorf() 的封装,原始 error 类型发生了改变,这就导致 err == sql.ErrNoRows 不再成立,返回信息变成了 Unknown error

如果想根据返回的 error 类型做不同处理,就无法实现了。

因此,Go 1.13 为我们提供了 wrapError 来处理这个问题。

Wrap Error

看一个例子:

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. type myError struct{}
  6. func (e myError) Error() string {
  7. return "Error happended"
  8. }
  9. func main() {
  10. e1 := myError{}
  11. e2 := fmt.Errorf("E2: %w", e1)
  12. e3 := fmt.Errorf("E3: %w", e2)
  13. fmt.Println(e2)
  14. fmt.Println(e3)
  15. }
  16. /* output
  17. E2: Error happended
  18. E3: E2: Error happended
  19. */

乍一看好像好没什么区别,但背后的实现原理却并不相同。

Go 扩展了 fmt.Errorf() 函数,增加了一个 %w 标识符来创建 wrapError

  1. // src/fmt/errors.go
  2. func Errorf(format string, a ...interface{}) error {
  3. p := newPrinter()
  4. p.wrapErrs = true
  5. p.doPrintf(format, a)
  6. s := string(p.buf)
  7. var err error
  8. if p.wrappedErr == nil {
  9. err = errors.New(s)
  10. } else {
  11. err = &wrapError{s, p.wrappedErr}
  12. }
  13. p.free()
  14. return err
  15. }

当使用 w% 时,函数会返回 &wrapError{s, p.wrappedErr}wrapError 结构体定义如下:

  1. // src/fmt/errors.go
  2. type wrapError struct {
  3. msg string
  4. err error
  5. }
  6. func (e *wrapError) Error() string {
  7. return e.msg
  8. }
  9. func (e *wrapError) Unwrap() error {
  10. return e.err
  11. }

实现了 Error() 方法,说明它是一个 error,而 Unwrap() 方法是为了获取被封装的 error

  1. // src/errors/wrap.go
  2. func Unwrap(err error) error {
  3. u, ok := err.(interface {
  4. Unwrap() error
  5. })
  6. if !ok {
  7. return nil
  8. }
  9. return u.Unwrap()
  10. }

它们之间的关系是这样的:

因此,我们可以使用 w% 将上文中的程序进行改造,使其内容输出更丰富。

如下:

  1. package main
  2. import (
  3. "database/sql"
  4. "errors"
  5. "fmt"
  6. )
  7. func bar() error {
  8. if err := foo(); err != nil {
  9. return fmt.Errorf("bar failed: %w", foo())
  10. }
  11. return nil
  12. }
  13. func foo() error {
  14. return fmt.Errorf("foo failed: %w", sql.ErrNoRows)
  15. }
  16. func main() {
  17. err := bar()
  18. if errors.Is(err, sql.ErrNoRows) {
  19. fmt.Printf("data not found, %+v\n", err)
  20. return
  21. }
  22. if err != nil {
  23. fmt.Println("Unknown error")
  24. }
  25. }
  26. /* output
  27. data not found, bar failed: foo failed: sql: no rows in result set
  28. */

终于有了让人满意的输出结果,每个函数都增加了必要的上下文信息,而且也符合对错误类型的判断。

errors.Is() 函数用来判断 err 以及其封装的 error 链中是否包含目标类型。这也就解决了上文提出的无法判断错误类型的问题。

后记

其实,Go 目前对 Error 的处理方式也是充满争议的。不过,官方团队正在积极和社区交流,提出改进方法。相信在不久的将来,一定会找到更好的解决方案。

现阶段来说,大部分团队可能会选择 github.com/pkg/errors 包来进行错误处理。如果感兴趣的话,可以学学看。

好了,本文就到这里吧。关注我,带你通过问题读 Go 源码。


源码地址:

推荐阅读:

参考文章:

原文链接:http://www.cnblogs.com/alwaysbeta/p/15801639.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号