Go Error 嵌套到底是怎么实现的?

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

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

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

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

// src/builtin/builtin.go

type error interface {
    Error() string
}

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

下面首先说说如何创建 error

创建 Error

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

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

errors.New()

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

举一个例子:

package main

import (
    "errors"
    "fmt"
)

func main() {
    err := errors.New("这是 errors.New() 创建的错误")
    fmt.Printf("err 错误类型:%T,错误为:%v\n", err, err)
}

/* 输出
err 错误类型:*errors.errorString,错误为:这是 errors.New() 创建的错误
*/

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

源码如下:

// src/errors/errors.go

// New returns an error that formats as the given text.

// Each call to New returns a distinct error value even if the text is identical.

func New(text string) error {
    return &errorString{text}
}

// errorString is a trivial implementation of error.

type errorString struct {
    s string
}

func (e *errorString) Error() string {
    return e.s
}

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

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

下面看第二种方式。

fmt.Errorf()

还是先看一个例子:

package main

import (
    "database/sql"
    "fmt"
)

func foo() error {
    return sql.ErrNoRows
}

func bar() error {
    return foo()
}

func main() {
    err := bar()
    if err == sql.ErrNoRows {
        fmt.Printf("data not found, %+v\n", err)
        return
    }
    if err != nil {
        fmt.Println("Unknown error")
    }
}

/* 输出
data not found, sql: no rows in result set
*/

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

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

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

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

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

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

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

Wrap Error

看一个例子:

package main

import (
    "fmt"
)

type myError struct{}

func (e myError) Error() string {
    return "Error happended"
}

func main() {
    e1 := myError{}
    e2 := fmt.Errorf("E2: %w", e1)
    e3 := fmt.Errorf("E3: %w", e2)
    fmt.Println(e2)
    fmt.Println(e3)
}

/* output
E2: Error happended
E3: E2: Error happended
*/

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

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

// src/fmt/errors.go

func Errorf(format string, a ...interface{}) error {
    p := newPrinter()
    p.wrapErrs = true
    p.doPrintf(format, a)
    s := string(p.buf)
    var err error
    if p.wrappedErr == nil {
        err = errors.New(s)
    } else {
        err = &wrapError{s, p.wrappedErr}
    }
    p.free()
    return err
}

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

// src/fmt/errors.go

type wrapError struct {
    msg string
    err error
}

func (e *wrapError) Error() string {
    return e.msg
}

func (e *wrapError) Unwrap() error {
    return e.err
}

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

// src/errors/wrap.go

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

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

Go Error 嵌套到底是怎么实现的?

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

如下:

package main

import (
    "database/sql"
    "errors"
    "fmt"
)

func bar() error {
    if err := foo(); err != nil {
        return fmt.Errorf("bar failed: %w", foo())
    }
    return nil
}

func foo() error {
    return fmt.Errorf("foo failed: %w", sql.ErrNoRows)
}

func main() {
    err := bar()
    if errors.Is(err, sql.ErrNoRows) {
        fmt.Printf("data not found,  %+v\n", err)
        return
    }
    if err != nil {
        fmt.Println("Unknown error")
    }
}

/* output
data not found,  bar failed: foo failed: sql: no rows in result set
*/

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

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

后记

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

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

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

源码地址:

推荐阅读:

参考文章:

Original: https://www.cnblogs.com/alwaysbeta/p/15801639.html
Author: yongxinz
Title: Go Error 嵌套到底是怎么实现的?

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/516470/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • Golang 源码解读 01、深入解析 strings.Builder、strings.Join

    本文从我的《The Go Programming Language》学习笔记中分离出,单独成一篇文章方便查阅参考。 strings.Builder 源码解析 存在意义 使用 (st…

    Go语言 2023年5月25日
    071
  • 基于Go语言实现好用的HTTP接口请求requests

    使用Go自带的net/http库可以发送各种HTTP请求。然而各种类型请求发送方式有点不太一致,这里参考Python requests库的使用方式,简单封装了一下。代码如下: 文件…

    Go语言 2023年5月29日
    054
  • 【golang】pprof性能调优工具的具体使用(带案例)

    前言 大晚上的,老是刷到有关pprof的文章,忍不住看了几篇文章…写个学习笔记记录下~ 正文: 1.pprof是什么? pprof是go内置的性能调优工具,可以借助一些…

    Go语言 2023年5月25日
    067
  • strchecker——Go源码字符串规范检查lint工具

    1.背景 在大型项目开发过程中,经常会遇到打印大量日志,输出信息和在源码中写注释的情况。对于软件开发来说,我们一般都是打印输出英文的日志(主要考虑软件在各种环境下的兼容性,如果打印…

    Go语言 2023年5月25日
    037
  • 对不起,我错了,这代码不好写

    hello,大家好呀,我是小楼。 前几天不是写了这篇文章《发现一个开源项目优化点,点进来就是你的了》嘛。 文章介绍了Sentinl的自适应缓存时间戳算法,从原理到实现都手把手解读了…

    Go语言 2023年5月25日
    057
  • 字符集与编码

    一个比特(bit)可以是0,或者是1,8个比特(bit),组成一个字节(byte)。全为0时代表数字0,全为1时代表数字255。 一个字节可以表示256个数字,两个字节可以表示65…

    Go语言 2023年5月25日
    041
  • golang实现一个简单的websocket聊天室

    基本原理:1.引入了 golang.org/x/net/websocket 包。2.监听端口。3.客户端连接时,发送结构体: {“type”:”…

    Go语言 2023年5月25日
    061
  • golang的defer踩坑汇总

    变量捕获 defer中的变量会被提前捕获,后续的修改不会影响到已捕获的值,举个例子: 结果defer语句中打印的值是修改前的值。: 最后输出值: 10 Defer运行值: 0 变量…

    Go语言 2023年5月25日
    045
  • Go语言之结构体与方法

    结构体是一系列属性的集合(类似于 Python 中的类) 1、结构体的定义与使用 // 定义 type Person struct { Name string Age int Se…

    Go语言 2023年5月25日
    056
  • 微服务追踪SQL(支持Isto管控下的gorm查询追踪)

    效果图 SQL的追踪正确插入到微服务的调用链之间 详细记录了SQL的执行内容和消耗时间 搜索SQL的类型 多线程(goroutine)下的追踪效果 在 Kubernetes 中部署…

    Go语言 2023年5月25日
    055
  • Go语言之如何给*int32类型赋值

    直接上代码: package main import “fmt” func main () { var num *int32 var i int32 = 5…

    Go语言 2023年5月29日
    042
  • Go Micro Dashboard – 实现细节(一)

    前言 Go Micro Dashboard是基于go-micro和ng-alain开发的, 它既是go-micro开发过程中的工具,也可以作为学习go-micro的实际案例。接下来…

    Go语言 2023年5月25日
    063
  • Go语言之高级篇Beego框架之爬虫项目实战

    一、爬虫项目 1、爬虫基础 a、网页上面会有相同的数据 b、去重处理 布隆过滤器哈希存储 c、标签匹配: 正则表达式beautiful soup或lxml这种标签提取库 d、动态内…

    Go语言 2023年5月29日
    050
  • 常见的限流算法

    通过限制并发访问数或者限制一个时间窗口内允许处理的请求数量来保护系统,例如,通过限流,你可以过滤掉产生流量峰值的客户和服务。 令牌桶算法 令牌桶算法是常见的一种限流算法。假设有一个…

    Go语言 2023年5月25日
    047
  • Go语言之高级篇beego框架之controller调用model

    一、controller调用model 开发规范,就该把对数据库的操作写在model文件夹中。 示例: views/main.go routers/router.go models…

    Go语言 2023年5月29日
    055
  • Go语言之接口

    接口就是一系列方法的集合(规范行为) 在面向对象的领域里,接口一般这样定义:接口定义一个对象的行为,规范子类对象的行为。 在 Go 语言中的接口是非侵入式接口(接口没了,不影响代码…

    Go语言 2023年5月25日
    052
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球