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

本文涉及的产品
云原生内存数据库 Tair,内存型 2GB
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
简介: 技术经验分享: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
相关文章
|
2天前
|
JSON Go 数据格式
技术经验分享:Golang如何解组嵌套的JSON数据的子集
技术经验分享:Golang如何解组嵌套的JSON数据的子集
|
12天前
|
移动开发 Go
golang bufio包怎么用?
`bufio` 是 Go 语言中用于提高 I/O 性能的包,它通过使用缓冲区减少对低效磁盘 I/O 操作的调用。简而言之,`bufio` 提供带缓冲的读写功能,减少读取或写入文件时的系统调用次数,从而提升程序性能。
|
1月前
|
Go
Golang标准库sync的使用
Golang标准库sync的使用
25 2
|
1月前
|
Shell Go API
7天玩转 Golang 标准库之 os
7天玩转 Golang 标准库之 os
25 1
|
1月前
|
监控 算法 Go
Golang深入浅出之-Go语言中的服务熔断、降级与限流策略
【5月更文挑战第4天】本文探讨了分布式系统中保障稳定性的重要策略:服务熔断、降级和限流。服务熔断通过快速失败和暂停故障服务调用来保护系统;服务降级在压力大时提供有限功能以保持整体可用性;限流控制访问频率,防止过载。文中列举了常见问题、解决方案,并提供了Go语言实现示例。合理应用这些策略能增强系统韧性和可用性。
123 0
|
1月前
|
前端开发 Go
Golang深入浅出之-Go语言中的异步编程与Future/Promise模式
【5月更文挑战第3天】Go语言通过goroutines和channels实现异步编程,虽无内置Future/Promise,但可借助其特性模拟。本文探讨了如何使用channel实现Future模式,提供了异步获取URL内容长度的示例,并警示了Channel泄漏、错误处理和并发控制等常见问题。为避免这些问题,建议显式关闭channel、使用context.Context、并发控制机制及有效传播错误。理解并应用这些技巧能提升Go语言异步编程的效率和健壮性。
66 5
Golang深入浅出之-Go语言中的异步编程与Future/Promise模式
|
1月前
|
Prometheus 监控 Cloud Native
Golang深入浅出之-Go语言中的分布式追踪与监控系统集成
【5月更文挑战第4天】本文探讨了Go语言中分布式追踪与监控的重要性,包括追踪的三个核心组件和监控系统集成。常见问题有追踪数据丢失、性能开销和监控指标不当。解决策略涉及使用OpenTracing或OpenTelemetry协议、采样策略以及聚焦关键指标。文中提供了OpenTelemetry和Prometheus的Go代码示例,强调全面可观测性对微服务架构的意义,并提示选择合适工具和策略以确保系统稳定高效。
166 5
|
1月前
|
监控 负载均衡 算法
Golang深入浅出之-Go语言中的协程池设计与实现
【5月更文挑战第3天】本文探讨了Go语言中的协程池设计,用于管理goroutine并优化并发性能。协程池通过限制同时运行的goroutine数量防止资源耗尽,包括任务队列和工作协程两部分。基本实现思路涉及使用channel作为任务队列,固定数量的工作协程处理任务。文章还列举了一个简单的协程池实现示例,并讨论了常见问题如任务队列溢出、协程泄露和任务调度不均,提出了解决方案。通过合理设置缓冲区大小、确保资源释放、优化任务调度以及监控与调试,可以避免这些问题,提升系统性能和稳定性。
65 6
|
1月前
|
负载均衡 算法 Go
Golang深入浅出之-Go语言中的服务注册与发现机制
【5月更文挑战第4天】本文探讨了Go语言中服务注册与发现的关键原理和实践,包括服务注册、心跳机制、一致性问题和负载均衡策略。示例代码演示了使用Consul进行服务注册和客户端发现服务的实现。在实际应用中,需要解决心跳失效、注册信息一致性和服务负载均衡等问题,以确保微服务架构的稳定性和效率。
37 3
|
1月前
|
安全 Go
Golang深入浅出之-Go语言中的并发安全队列:实现与应用
【5月更文挑战第3天】本文探讨了Go语言中的并发安全队列,它是构建高性能并发系统的基础。文章介绍了两种实现方法:1) 使用`sync.Mutex`保护的简单队列,通过加锁解锁确保数据一致性;2) 使用通道(Channel)实现无锁队列,天生并发安全。同时,文中列举了并发编程中常见的死锁、数据竞争和通道阻塞问题,并给出了避免这些问题的策略,如明确锁边界、使用带缓冲通道、优雅处理关闭以及利用Go标准库。
42 5