Go Error 嵌套实现创建方式

目录
  • 创建 Error
    • errors.New()
    • fmt.Errorf()
    • Wrap 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()
}

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

因此,我们可以使用 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 源码。

源码地址:

https://github.com/yongxinz/gopher

推荐阅读:

为什么要避免在 Go 中使用 ioutil.ReadAll?

如何在 Go 中将 []byte 转换为 io.Reader?

开始读 Go 源码了

参考文章:

https://chasecs.github.io/posts/the-philosophy-of-go-error-handling/

https://medium.com/@dche423/golang-error-handling-best-practice-cn-42982bd72672

https://www.flysnow.org/2019/09/06/go1.13-error-wrapping.html

到此这篇关于Go Error 嵌套到底是怎么实现的?的文章就介绍到这了,更多相关Go Error 嵌套内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

时间: 2022-01-12

浅谈Golang 嵌套 interface 的赋值问题

大家还是直接看代码吧~ package main import ( "fmt" ) func main() { s := map[string]interface{}{ "code":0, "msg":"", "data":map[string]interface{}{ "src":"", }, } s["data"].(map[string]in

go嵌套匿名结构体的初始化详解

go匿名结构体 嵌套匿名结构体的 示例代码片. type debugConfig struct { MaxQueueDepth int `json:"maxQueueDepth"` ListenerEntries string `json:"listenerEntries"` Listeners string Logging struct { Info string `json:"info"` Protocol string `json:&quo

golang 接口嵌套实现复用的操作

大家还是直接看代码吧~ package main import ( "fmt" ) func main() { start(NewB(C{})) start(NewB(D{})) } type A interface { what() } type B struct { A } type C struct { } func (b C) what() { fmt.Println("this is type C") } type D struct { } func (b

go结构体嵌套的切片数组操作

看代码吧~ package main import ( "fmt" ) type XCDataStu struct { Id int `json:"id" xorm:"id"` Name string `json:"name" xorm:"name"` } type XCDataStu1 struct { Id int `json:"id" xorm:"id"` St

C语言结构体(struct)常见使用方法(细节问题)

基本定义:结构体,通俗讲就像是打包封装,把一些有共同特征(比如同属于某一类事物的属性,往往是某种业务相关属性的聚合)的变量封装在内部,通过一定方法访问修改内部变量. 结构体定义: 第一种:只有结构体定义 struct stuff{ char job[20]; int age; float height; }; 第二种:附加该结构体类型的"结构体变量"的初始化的结构体定义 //直接带变量名Huqinwei struct stuff{ char job[20]; int age; floa

C语言中的结构体的入门学习教程

C语言中数组允许定义类型的变量,可容纳相同类型的多个数据项,但结构体在C语言编程中,它允许定义不同种类的数据项可供其他用户定义的数据类型. 结构是用来代表一个记录,假设要跟踪图书馆的书籍.可能要跟踪有关每本书以下属性: Title - 标题 Author - 作者 Subject - 科目 Book ID - 编号 定义结构体 定义一个结构体,必须使用结构体的struct语句.该struct语句定义了一个新的数据类型,程序不止一个成员.struct语句的格式是这样的: struct [struc

C语言 结构体和指针详解及简单示例

指针也可以指向一个结构体,定义的形式一般为: struct 结构体名 *变量名; 下面是一个定义结构体指针的实例: struct stu{ char *name; //姓名 int num; //学号 int age; //年龄 char group; //所在小组 float score; //成绩 } stu1 = { "Tom", 12, 18, 'A', 136.5 }; //结构体指针struct stu *pstu = &stu1; 也可以在定义结构体的同时定义结构

C#如何利用结构体对固定格式数据进行解析

本文为大家分享了C#利用结构体解析固定格式数据的具体代码,供大家参考,具体内容如下 制定了一个通讯协议,然后其数据部分有如下格式. 第三列代表的是字节数,第4列是数据类型. 当传输或者收到一个byte数组的时候(下面Hex数据),按照对应格式进行解析,解析方法有很多种,网上看到了一种方式是以结构体的方式来解析的,类似C/C++方式. Hex数据:01 01 00 00 10 44 65 76 69 63 65 20 4E 61 6D 65 31 00 00 00 00 00 00 00 00 0

JS数组操作(数组增加、删除、翻转、转字符串、取索引、截取(切片)slice、剪接splice、数组合并)

POP 删除最后一项 删除最后一项,并返回删除元素的值:如果数组为空则返回undefine var a = [1,2,3,4,5]; a.pop();//a:[1, 2, 3, 4] a.pop();//a:[1, 2, 3] a.pop();//a:[1, 2] shift 删除第一项 删除原数组第一项,并返回删除元素的值:如果数组为空则返回undefine var a = [1,2,3,4,5]; a.shift(); //a:[2,3,4,5] a.shift(); //a:[3, 4,

C语言 结构体数组详解及示例代码

所谓结构体数组,是指数组中的每个元素都是一个结构体.在实际应用中,结构体数组常被用来表示一个拥有相同数据结构的群体,比如一个班的学生.一个车间的职工等. 定义结构体数组和定义结构体变量的方式类似,请看下面的例子: struct stu{ char *name; //姓名 int num; //学号 int age; //年龄 char group; //所在小组 float score; //成绩 }class[5]; 表示一个班级有5个学生. 结构体数组在定义的同时也可以初始化,例如: str

详解C++中的指针结构体数组以及指向结构体变量的指针

C++结构体数组 一个结构体变量中可以存放一组数据(如一个学生的学号.姓名.成绩等数据).如果有10个学生的数据需要参加运算,显然应该用数组,这就是结构体数组.结构体数组与以前介绍过的数值型数组的不同之处在于:每个数组元素都是一个结构体类型的数据,它们都分别包括各个成员项. 定义结构体数组和定义结构体变量的方法相仿,定义结构体数组时只需声明其为数组即可.如: struct Student //声明结构体类型Student { int num; char name[20]; char sex; i

C#调用C++DLL传递结构体数组的终极解决方案

C#调用C++DLL传递结构体数组的终极解决方案 在项目开发时,要调用C++封装的DLL,普通的类型C#上一般都对应,只要用DllImport传入从DLL中引入函数就可以了.但是当传递的是结构体.结构体数组或者结构体指针的时候,就会发现C#上没有类型可以对应.这时怎么办,第一反应是C#也定义结构体,然后当成参数传弟.然而,当我们定义完一个结构体后想传递参数进去时,会抛异常,或者是传入了结构体,但是返回值却不是我们想要的,经过调试跟踪后发现,那些值压根没有改变过,代码如下. [DllImport(

C++结构体数组详细解析

1.定义结构体数组 和定义结构体变量类似,定义结构体数组时只需声明其为数组即可.如: 复制代码 代码如下: struct Student{     int num;     char name[20];     char sex[5];     int age;     float score;     char addr[30];};Student stu[3]; //定义Student类型的数组stu 2.结构体数组的应用举例 题目:对候选人的票的统计程序. 设有3个候选人,最终只能有一个当

C#中结构体和字节数组转换实现

最近在使用结构体与字节数组转化来实现socket间数据传输.现在开始整理一下.对于Marshal可以查阅msdn,关于字节数组与结构体转代码如下: using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.IO; using System.Runtime.InteropServices; namespace FileSendClient { [StructL