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语言reflect包的反射机制基本用法示例

    Go语言reflect包的反射机制基本用法示例

    反射在处理接口和类型断言、开发通用功能或者设计框架时尤为重要,本文将深入探索 Go 语言中的反射机制,通过具体的示例展示如何使用 reflect 包,让你能够在 Go 项目中有效地利用这一强大的工具
    2023-11-11
  • GOLang IO接口与工具使用方法讲解

    GOLang IO接口与工具使用方法讲解

    这篇文章主要介绍了GOLang IO接口与工具使用方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习吧
    2023-03-03
  • golang如何使用gos7读取S7200Smart数据

    golang如何使用gos7读取S7200Smart数据

    文章介绍了如何使用Golang语言的Gos7工具库读取西门子S7200Smart系列PLC的数据,通过指定数据块号、起始字节偏移量和数据长度,可以精确读取所需的数据,感兴趣的朋友跟随小编一起看看吧
    2024-12-12
  • go编译标签build tag注释里语法详解

    go编译标签build tag注释里语法详解

    这篇文章主要为大家介绍了go编译标签build tag注释里语法详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-09-09
  • Go高效率开发Web参数校验三种方式实例

    Go高效率开发Web参数校验三种方式实例

    这篇文章主要介绍了Go高效率开发Web参数校验三种方式实例,需要的朋友可以参考下
    2022-11-11
  • Go语言的文件操作代码汇总

    Go语言的文件操作代码汇总

    本文给大家汇总介绍了go语言中的文件操作的代码,包括文件的读写,文件的新建打开和删除等,希望对大家学习go语言能够有所帮助
    2018-10-10
  • sublime text3解决Gosublime无法自动补全代码的问题

    sublime text3解决Gosublime无法自动补全代码的问题

    本文主要介绍了sublime text3解决Gosublime无法自动补全代码的问题,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-01-01
  • golang 中 recover()的使用方法

    golang 中 recover()的使用方法

    这篇文章主要介绍了Guam与golang  recover()的使用方法,Recover 是一个Go语言的内建函数,可以让进入宕机流程中的 goroutine 恢复过来,下文更多相关资料需要的小伙伴可以参考一下
    2022-04-04
  • 解析GOROOT、GOPATH、Go-Modules-三者的关系

    解析GOROOT、GOPATH、Go-Modules-三者的关系

    这篇文章主要介绍了解析GOROOT、GOPATH、Go-Modules-三者的关系,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-10-10
  • Go实现快速生成固定长度的随机字符串

    Go实现快速生成固定长度的随机字符串

    这篇文章主要为大家详细介绍了怎样在Go中简单快速地生成固定长度的随机字符串,文中的示例代码讲解详细,具有一定的借鉴价值,需要的可以学习一下
    2022-10-10

最新评论