Golang标准库之errors包应用方式

 更新时间:2024年10月19日 14:55:32   作者:一只coding猪  
Go语言的errors包提供了基础的错误处理能力,允许通过errors.New创建自定义error对象,error在Go中是一个接口,通过实现Error方法来定义错误文本,对错误的比较通常基于对象地址,而非文本内容,因此即使两个错误文本相同

一. errors的基本应用

errors包是一个比较简单的包,包括常见的errors.New创建一个error对象,或通过error.Error方法获取error中的文本内容,本质上在builtin类型中,error被定义为一个interface,这个类型只包含一个Error方法,返回字符串形式的错误内容。

应用代码很简单:

// 示例代码
func Oops() error {
	return errors.New("iam an error")
}

func Print() {
	err := Oops()
	fmt.Println("oops, we go an error,", err.Error())
}

通过errors.New方法,可以创建一个error对象,在标准库实现中,对应了一个叫errorString的实体类型,是对error接口的最基本实现。

二. 错误类型的比较

代码中经常会出现err == nil 或者err == ErrNotExist之类的判断,对于error类型,由于其是interface类型,实际比较的是interface接口对象实体的地址。

也就是说,重复的new两个文本内容一样的error对象,这两个对象并不相等,因为比较的是这两个对象的地址。这是完全不同的两个对象

// 展示了error比较代码
if errors.New("hello error") == errors.New("hello error") { // false
}
errhello := errors.New("hello error")
if errhello == errhello { // true
}

在通常的场景中,能掌握errors.New()、error.Error()以及error对象的比较,就能应付大多数场景了,但是在大型系统中,内置的error类型很难满足需要,所以下面要讲的是对error的扩展。

三. error的扩展

3.1 自定义error

go允许函数具有多返回值,但通常你不会想写太多的返回值在函数定义上(looks ugly),而标准库内置的errorString类型由于只能表达字符串错误信息显然受限。所以,可以通过实现error接口的方式,来扩展错误返回

// 自定义error类型
type EasyError struct {
	Msg  string	// 错误文本信息
	Code int64	// 错误码
}

func (me *EasyError) Error() string {
	// 当然,你也可以自定义返回的string,比如
	// return fmt.Sprintf("code %d, msg %s", me.Code, me.Msg)
	return me.Msg
}

// Easy实现了error接口,所以可以在Oops中返回
func DoSomething() error {
	return &EasyError{"easy error", 1}
}

// 业务应用
func DoBusiness() {
	err := DoSomething()
	e,ok := err.(EasyError)
	if ok {
		fmt.Printf("code %d, msg %s\n", e.Code, e.Msg)
	}
}

现在在自定义的错误类型中塞入了错误码信息。随着业务代码调用层层深入,当最内层的操作(比如数据库操作)发生错误时,我们希望能在业务调用链上每一层都携带错误信息,就像递归调用一样,这时可以用到标准库的Unwrap方法

3.2 Unwrap与Nested error

一旦你的自定义error实现类型定义了Unwrap方法,那么它就具有了嵌套的能力,其函数原型定义如下:

// 标准库Unwrap方法,传入一个error对象,返回其内嵌的error
func Unwrap(err error) error

// 自定义Unwrap方法
func (me *EasyError) Unwrap() error {
	// ... 
}

虽然error接口没有定义Unwrap方法,但是标准库的Unwrap方法中会通过反射隐式调用自定义类型的Unwrap方法,这也是业务实现自定义嵌套的途径。我们给EasyError增加一个error成员,表示包含的下一级error

// 
type EasyError struct {
	Msg  string	// 错误文字信息
	Code int64	// 错误码
	Nest error 	// 嵌套的错误
}

func (me *EasyError) Unwrap() error {
	return me.Nest
}

func DoSomething1() error {
	// ...
	err := DoSomething2()
	if err != nil {
		return &EasyError{"from DoSomething1", 1, err}
	}

	return nil
}

func DoSomething2() error {
	// ...
	err := DoSomething3()
	if err != nil {
		return &EasyError{"from DoSomething2", 2, err}
	}

	return nil
}

func DoSomething3() error {
	// ...

	return &EasyError{"from DoSomething3", 3, nil}
}
// 可以很清楚的看到调用链上产生的错误信息
// Output:
// 	code 1, msg from DoSomething1
// 	code 2, msg from DoSomething2
// 	code 3, msg from DoSomething3
func main() {
	err := DoSomething1()
	for err != nil {
		e := err.(*EasyError)
		fmt.Printf("code %d, msg %s\n", e.Code, e.Msg)
		err = errors.Unwrap(err)		// errors.Unwrap中调用EasyError的Unwrap返回子error
	}
}

输出如下

$ ./sample
code 1, msg from DoSomething1
code 2, msg from DoSomething2
code 3, msg from DoSomething3

这样就可以在深入的调用链中,通过嵌套的方式,将调用路径中的错误信息,携带至调用栈的栈底。

对于不同模块,返回的错误信息大不相同,比如网络通信模块期望错误信息携带http状态码,而数据持久层期望返回sql或redis commend,随着模块化的职能划分,每个子模块可能会定义自己的自定义error类型,这时在业务上去区分不同类别的错误,就可以使用Is方法

3.3 errors.Is方法与错误分类

以网络错误和数据库错误为例,分别定义两种实现error接口的结构NetworkError和DatabaseError。

// 网络接口返回的错误类型
type NetworkError struct {
	Code   int	  // 10000 - 19999
	Msg    string // 文本信息
	Status int    // http状态码
}

// 数据库模块接口返回的错误类型
type DatabaseError struct {
	Code int	// 20000 - 29999
	Msg  string // 文本错误信息
	Sql  string // sql string
}

NetworkError与DatabaseError都实现了Error方法和Unwrap方法,代码里就不重复写了。错误类型的划分,导致上层业务对error的处理产生变化:业务层需要知道发生了什么,才能给用户提供恰当的提示,但是又不希望过分详细,比如用户期望看到的是“数据访问异常”、“请检查网络状态”,而不希望用户看到“unknown column space in field list…”、“request timeout…”之类的技术性错误信息。此时Is方法就派上用场了。

现在我们为网络或数据库错误都增加一个Code错误码,并且人为对错误码区间进行划分,[10000,20000)表示网络错误,[20000,30000)表示数据库错误,我们期望在业务层能够知道错误码中是否包含网络错误或数据访问错误,还需要为两种错误类型添加Is方法:

var(
	// 将10000和20000预留,用于在Is方法中判断错误码区间
	ErrNetwork  = &NetworkError{EasyError{"", 10000, nil}, 0}
	ErrDatabase = &DatabaseError{EasyError{"", 20000, nil}, ""}
)

func (ne NetworkError) Is(e error) bool {
	err, ok := e.(*NetworkError)
	if ok {
		start := err.Code / 10000
		return ne.Code >= 10000 && ne.Code < (start+1)*10000
	}
	return false
}

func (de DatabaseError) Is(e error) bool {
	err, ok := e.(*DatabaseError)
	if ok {
		start := err.Code / 10000
		return de.Code >= 10000 && de.Code < (start+1)*10000
	}
	return false
}

与Unwrap类似,Is方法也是被errors.Is方法隐式调用的,来看一下业务代码

func DoNetwork() error {
	// ...
	return &NetworkError{EasyError{"", 10001, nil}, 404}
}

func DoDatabase() error {
	// ...
	return &DatabaseError{EasyError{"", 20003, nil}, "select 1"}
}

func DoSomething() error {
	if err := DoNetwork(); err != nil {
		return err
	}
	if err := DoDatabase(); err != nil {
		return err
	}
	return nil
}

func DoBusiness() error {
	err := DoSomething()
	if err != nil {
		if errors.Is(err, ErrNetworks) {
			fmt.Println("网络异常")
		} else if errors.Is(err, ErrDatabases) {
			fmt.Println("数据访问异常")
		}
	} else {
		fmt.Println("everything is ok")
	}
	return nil
}

执行DoBusiness,输出如下:

$ ./sample
网络异常

通过Is方法,可以将一批错误信息归类,对应用隐藏相关信息,毕竟大部分时候,我们不希望用户直接看到出错的sql语句。

3.4 errors.As方法与错误信息读取

现在通过Is实现了分类,可以判断一个错误是否是某个类型,但是更进一步,如果我们想得到不同错误类型的详细信息呢?业务层拿到返回的error,就不得不通过层层Unwrap和类型断言来获取调用链中的深层错误信息。所以errors包提供了As方法,在Unwrap的基础上,直接获取error接口中,实际是error链中指定类型的错误。

所以在DatabaseError的基础上,再定义一个RedisError类型,作为封装redis访问异常的类型

// Redis模块接口返回的错误类型
type RedisError struct {
	EasyError
	Command string // redis commend
	Address string // redis instance address
}

func (re *RedisError) Error() string {
	return re.Msg
}

在业务层,尝试读取数据库和redis错误的详细信息

func DoDatabase() error {
	// ...
	return &DatabaseError{EasyError{"", 20003, nil}, "select 1"}
}

func DoRedis() error {
	// ...
	return &RedisError{EasyError{"", 30010, nil}, "set hello 1", "127.0.0.1:6379"}
}

func DoDataWork() error {
	if err := DoRedis(); err != nil {
		return err
	}
	if err := DoDatabase(); err != nil {
		return err
	}
	return nil
}

// 执行业务代码
func DoBusiness() {
	err := DoDataWork()
	if err != nil {
		if rediserr := (*RedisError)(nil); errors.As(err, &rediserr) {
			fmt.Printf("Redis exception, commend : %s, instance : %s\n", rediserr.Command, rediserr.Address)
		} else if mysqlerr := (*DatabaseError)(nil); errors.As(err, &mysqlerr) {
			fmt.Printf("Mysql exception, sql : %s\n", mysqlerr.Sql)
		}
	} else {
		fmt.Println("everything is ok")
	}
}

运行DoBusiness,输出如下

$ ./sample
Redis exception, commend : set hello 1, instance : 127.0.0.1:6379

conclusion

  • error是interface类型,可以实现自定义的error类型
  • error支持链式的组织形式,通过自定义Unwrap实现对error链的遍历
  • errors.Is用于判定error是否属于某类错误,归类方式可以在自定义error的Is方法中实现
  • errors.As同样可以用于判断error是否属于某个错误,避免了显式的断言处理,并同时返回使用该类型错误表达的错误信息详情
  • 无论是Is还是As方法,都会尝试调用Unwrap方法递归地查找错误,所以如果带有Nesty的错误,务必要实现Unwrap方法才可以正确匹配

通过这些手段,可以在不侵入业务接口的情况下,丰富错误处理,这就是errors包带来的便利。

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • 一文告诉你大神是如何学习Go语言之make和new

    一文告诉你大神是如何学习Go语言之make和new

    当我们想要在 Go 语言中初始化一个结构时,其实会使用到两个完全不同的关键字,也就是 make 和 new,同时出现两个用于『初始化』的关键字对于初学者来说可能会感到非常困惑,不过它们两者有着却完全不同的作用,本文就和大家详细讲讲
    2023-02-02
  • Go连接并操作SQLite数据库基本步骤

    Go连接并操作SQLite数据库基本步骤

    在这篇文章中,我们将详细描述如何在 Go 语言中使用 SQLite 数据库,我们将会从如何在 Go 中安装和使用 SQLite 驱动包开始讲起,然后逐步介绍如何创建数据库连接,执行 SQL 查询,处理返回的数据以及关闭数据库连接
    2024-01-01
  • Go语言包管理模式示例分析

    Go语言包管理模式示例分析

    这篇文章主要为大家介绍了Go语言包管理模式示例分析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-05-05
  • 使用Go语言连接和操作数据库的基本步骤

    使用Go语言连接和操作数据库的基本步骤

    在Go语言中,连接和操作数据库通常使用database/sql包,它提供了一个数据库抽象层,支持多种数据库引擎,如MySQL、PostgreSQL、SQLite等,下面我将以MySQL为例,详细讲解如何使用Go语言连接和操作数据库,需要的朋友可以参考下
    2024-06-06
  • Go语言CSP并发模型goroutine及channel底层实现原理

    Go语言CSP并发模型goroutine及channel底层实现原理

    这篇文章主要为大家介绍了Go语言CSP并发模型goroutine channel底层实现原理,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-05-05
  • Go语言实现逐行读取和写入文件详解

    Go语言实现逐行读取和写入文件详解

    这篇文章主要介绍了如何使用go语言实现从输入文件中读取每行数据,然后将每行字段组合成SQL插入脚本,然后逐行写入另外一个空白文件中,有需要的可以参考下
    2024-01-01
  • 一文搞懂Go语言中defer关键字的使用

    一文搞懂Go语言中defer关键字的使用

    defer是golang中用的比较多的一个关键字,也是go面试题里经常出现的问题。今天就来整理一下关于defer的学习使用,希望对需要的朋友有所帮助
    2022-09-09
  • Go语言TCP从原理到代码实现详解

    Go语言TCP从原理到代码实现详解

    这篇文章主要为大家介绍了Go语言TCP从原理到代码实现详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-08-08
  • go micro集成链路跟踪的方法和中间件原理解析

    go micro集成链路跟踪的方法和中间件原理解析

    这篇文章主要介绍了go-micro集成链路跟踪的方法和中间件原理,关于Http或者说是Restful服务的链路跟踪,go-micro的httpClient支持CallWrapper,可以用WrapCall来添加链路跟踪的CallWrapper,本文给大家介绍的非常详细,需要的朋友参考下吧
    2022-05-05
  • Go语言中读取命令参数的几种方法总结

    Go语言中读取命令参数的几种方法总结

    部署golang项目时难免要通过命令行来设置一些参数,那么在golang中如何操作命令行参数呢?那么下面这篇文章就来给大家介绍了关于Go语言中读取命令参数的几种方法,文中通过示例代码介绍的非常详细,需要的朋友可以参考借鉴,下面随着小编来一起看看吧。
    2017-11-11

最新评论