技术经验分享:Golang标准库:errors包应用

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
云数据库 Tair(兼容Redis),内存型 2GB
简介: 技术经验分享:Golang标准库:errors包应用

一. 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 //代码效果参考:http://www.jhylw.com.cn/491731117.html

&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包带来的便利。

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
15天前
|
JSON Go 开发者
go-carbon v2.5.0 发布,轻量级、语义化、对开发者友好的 golang 时间处理库
carbon 是一个轻量级、语义化、对开发者友好的 Golang 时间处理库,提供了对时间穿越、时间差值、时间极值、时间判断、星座、星座、农历、儒略日 / 简化儒略日、波斯历 / 伊朗历的支持。
32 4
|
1月前
|
存储 Cloud Native Shell
go库介绍:Golang中的Viper库
Viper 是 Golang 中的一个强大配置管理库,支持环境变量、命令行参数、远程配置等多种配置来源。本文详细介绍了 Viper 的核心特点、应用场景及使用方法,并通过示例展示了其强大功能。无论是简单的 CLI 工具还是复杂的分布式系统,Viper 都能提供优雅的配置管理方案。
|
3月前
|
算法 安全 测试技术
golang 栈数据结构的实现和应用
本文详细介绍了“栈”这一数据结构的特点,并用Golang实现栈。栈是一种FILO(First In Last Out,即先进后出或后进先出)的数据结构。文章展示了如何用slice和链表来实现栈,并通过golang benchmark测试了二者的性能差异。此外,还提供了几个使用栈结构解决的实际算法问题示例,如有效的括号匹配等。
golang 栈数据结构的实现和应用
|
2月前
|
中间件 Go 数据处理
应用golang的管道-过滤器架构风格
【10月更文挑战第1天】本文介绍了一种面向数据流的软件架构设计模式——管道-过滤器(Pipe and Filter),并通过Go语言的Gin框架实现了一个Web应用示例。该模式通过将数据处理流程分解为一系列独立的组件(过滤器),并利用管道连接这些组件,实现了模块化、可扩展性和高效的分布式处理。文中详细讲解了Gin框架的基本使用、中间件的应用以及性能优化方法,展示了如何构建高性能的Web服务。
78 0
|
3月前
|
存储 监控 Go
面向OpenTelemetry的Golang应用无侵入插桩技术
文章主要讲述了阿里云 ARMS 团队与程序语言与编译器团队合作研发的面向OpenTelemetry的Golang应用无侵入插桩技术解决方案,旨在解决Golang应用监控的挑战。
|
3月前
|
Unix Go
Golang语言标准库time之日期和时间相关函数
这篇文章是关于Go语言日期和时间处理的文章,介绍了如何使用Go标准库中的time包来处理日期和时间。
59 3
|
3月前
|
存储 Go
Golang语言基于go module方式管理包(package)
这篇文章详细介绍了Golang语言中基于go module方式管理包(package)的方法,包括Go Modules的发展历史、go module的介绍、常用命令和操作步骤,并通过代码示例展示了如何初始化项目、引入第三方包、组织代码结构以及运行测试。
58 3
|
3月前
|
Go
Golang语言基于GOPATH方式管理包(package)
这篇文章详细介绍了Golang语言中基于GOPATH方式管理包(package)的方法,包括包的概述、定义、引入格式、别名使用、匿名引入,以及如何快速入门自定义包,并通过具体代码案例展示了包的环境准备、代码编写、细节说明和程序运行。
41 3
|
3月前
|
Go
Golang语言之包依赖管理
这篇文章详细介绍了Go语言的包依赖管理工具,包括godep和go module的使用,以及如何在项目中使用go module进行依赖管理,还探讨了如何导入本地包和第三方库下载的软件包存放位置。
41 3
|
3月前
|
Go
Golang语言之管道channel快速入门篇
这篇文章是关于Go语言中管道(channel)的快速入门教程,涵盖了管道的基本使用、有缓冲和无缓冲管道的区别、管道的关闭、遍历、协程和管道的协同工作、单向通道的使用以及select多路复用的详细案例和解释。
125 4
Golang语言之管道channel快速入门篇