88.Go设计优雅的错误处理
文章目录
- 导言
- 一、Go 的约定
- 二、简单错误创建
- 1、 errors.New()
- 2、fmt.Errorf()
- 三、哨兵错误
- 四、对错误进行编程
- 1、优雅的错误处理设计
- 2、与错误有关的的API
- 五、总结
导言
在 75.错误码设计、实现统一异常处理和封装统一返回结果 中,我们介绍了错误码的设计,包括错误码的分段式含义。本篇文章,我们将结合错误码以及错误msg介绍一下Go中优雅的错误处理是如何设计的。共有两种常见方式:
- 哨兵错误
- 定义结构体(
至少包含code和msg)两个字段
一、Go 的约定
首先咱们需要知道 Go 语言里面有个约定,就是一个方法的返回参数,我们通常习惯的把错误当最后一个参数返回。
这虽然官方在这点上没有做硬性规定,但是大家也都习惯这么做,至于为啥 Go 要这样去设计处理异常,想必是约定俗成,让大家有统一的写法,所以官方怎么设计咱们就怎么遵守就好了。
二、简单错误创建
Go 的标准库里面为我们提供了两种使用字符串快速创建错误的方式。
1、 errors.New()
我们可以使用errors包的 New 方法,传入一个字符串快速地创建。
var e error
e = errors.New("我是错误")
2、fmt.Errorf()
可能大多数同学都习惯用 fmt 去拼装一些内容,然后创建错误,省去了用fmt.Sprintf拼装字符串,然后再调用errors.New(),而是拼装msg和创建错误一步到位了。
var e error
e = fmt.Errorf("你好,%s", "我还是错误")
从这个角度可以看到,其实错误对 Go 语言来说,其实就是一段字符串。
三、哨兵错误
接下来我们分享 Go 中最常用的设计 error 的方式,那就是哨兵模式。
哨兵错误是计算机编程中使用一个特定的值来表示不可能进一步处理的做法,通常在Go语言编程中使用,用于在包内先定义一些错误,然后在包外进行错误值的判断。哨兵错误的注意事项在Go的官方博客中也提到,哨兵错误是包级别的,可以用于在包外进行错误值的判断,但这样会造成包和包之间的依赖,如果哨兵错误做了修改,那么之前依赖该错误的所有包都需要更改。
怎么去理解呢?
就像童话故事里一座城堡,在城堡的一些关卡,总会安排各种各样的哨兵,他们不同哨兵负责的事不同。
所以我们通常会在一个包里面设置一些标志性的错误,方便调用者对错误做更好的处理。
拿我们常用的 GORM 这个库吧,我们在查询某条数据的时候,如果没找到这条数据,不知道你是怎么判断的。
其实官方为我们提供了错误哨兵,在源码 github.com/jinzhu/gorm/errors.go中:
var (// ErrRecordNotFound returns a "record not found error". Occurs only when attempting to query the database with a struct; querying with a slice won't return this errorErrRecordNotFound = errors.New("record not found")// ErrInvalidSQL occurs when you attempt a query with invalid SQLErrInvalidSQL = errors.New("invalid SQL")// ErrInvalidTransaction occurs when you are trying to `Commit` or `Rollback`ErrInvalidTransaction = errors.New("no valid transaction")// ErrCantStartTransaction can't start transaction when you are trying to start one with `Begin`ErrCantStartTransaction = errors.New("can't start transaction")// ErrUnaddressable unaddressable valueErrUnaddressable = errors.New("using unaddressable value")
)
所以我们就可以直接通过返回的 error 来判断是不是没找到数据,伪代码如下:
g,_ := gorm.Open()
e = g.Find().Error
if e == gorm.ErrRecordNotFound {fmt.Println("没找到")
}
其实这样用 == 比较是有坑的,比如包内ErrRecordNotFound = errors.New("record not found")错误被gorm包的开发着删除了,这里的代码一升级包依赖,就会编译报错了,不过删除已经定义的错误这种情况一般不会出现。可以用Is解决这种情况,后面会介绍。
所以如果我们在写我们的模块的时候,也可以这样去设计我们的错误。
虽然这种设计模式网上也有很多人说不好,因为他建立起了两个包之间的依赖,说人话就是,如果我们要比较错误,就必须导入错误所在的包。
反正任何设计都会有人说好有人说坏,大家理智看到就好了。
四、对错误进行编程
我们需要时刻记住,Go 语言中错误其实就是一串字符串。
我们应该尽量避免去比较 error.Error() 输出的值,因为不同协作者写代码时返回的字符串(错误提示信息)很可能是不一样的。
Go 语言中的错误定义是一个接口,只要是声明了 Error() string 这个方法,就意味着它实现了Error接口。
这是 Go 中的错误定义源码:
// The error built-in interface type is the conventional interface for
// representing an error condition, with the nil value representing no error.
type error interface {Error() string
}
如果官方的错误,并不能满足你的需求,咱们也可以自定义。
1、优雅的错误处理设计
我们先来使用常量去创建自定义错误吧:
type MyError stringfunc (this MyError) Error() string {return string(this)
}
这样我们就创建好我们的自定义错误了,使用下:
func main() {var e errore = MyError("hello")fmt.Println(e)
}
当然我们可以把 string 换成 struct ,同时加入很多我们自定义的属性:工作中非常常用
type MyError struct {Code intMsg string
}func NewMyError(code int, msg string) *MyError {return &MyError{Code: code, Msg: msg}
}func (this MyError) Error() string {return fmt.Sprintf("%d-%s",this.Code, this.Msg)
}// FindUser 模拟下我们的业务方法
func FindUser() error {return NewMyError(404, "找不到内容")
}func main() {var e errore = FindUser()fmt.Println(e)
}
当然,实际工作中,一般还会用metrics打点记录错误情况,方便后续进行监控报警,而业务上有一些预期内的错误或者稳定性不需要处理的错误,我们希望不要报警。一方面可以在报警规则中过滤,另一方面我们其实可以在错误设计和上报的时候就打点,标记错误是否为稳定性错误。如下:
// 错误码枚举
type ErrorCodeEnum int64const (// 定义可预见的异常UserNotFound ErrCodeEnum = 10001PasswrodErr ErrCodeEnum = 10002
)type BizErr struct {code ErrCodeEnum msg stringisStable bool // 是否是稳定性错误
}func NewBizErr(code ErrCodeEnum, msg string) *OpStrategyErr {return &OpStrategyErr{code: code,msg: msg,isStable: ErrorCodeIsStable(code),}
}func (e *OpStrategyErr) Code() strategy_error.OpStrategyErrCode {return e.code
}func (e *OpStrategyErr) Error() string {return e.msg
}func (e *OpStrategyErr) IsStable() bool {return e.isStable
}func ErrorCodeIsStable(errCode ErrCodeEnum) bool {errCodeStr := strconv.FormatInt(int64(errCode), 10)if len(errCodeStr) < 4 {return false}// 假设以1000开头认为是稳定性错误if errCodeStr[:4] == "1000" {return true}return false
}func IsErrNil(err error) bool {if err == nil {return true}vi := reflect.ValueOf(err)if vi.Kind() == reflect.Ptr {return vi.IsNil()}return false
}
2、与错误有关的的API
最后我们来说说 Go 语言中与错误有关的 API,到目前为止,我们面对错误除了输出外,就是使用 == 对错误进行哨兵比较,但是这样未必准确。
所以官方在错误的基础上,又扩展了几个 API。
1、Is
我们面对错误,尽量不要使用这样的方式去比较:
// 尽量少用
if e.Error() == "404-找不到内容" {
}
尽量少用,最好不用。
也少用这样的方式:
type MyError struct {Code intMsg string
}func NewMyError(code int, msg string) *MyError {return &MyError{Code: code, Msg: msg}
}func (this MyError) Error() string {return fmt.Sprintf("%d-%s",this.Code, this.Msg)
}var ErrorNotFind = NewMyError(404, "找不到内容")// FindUser 模拟下我们的业务方法
func FindUser() error {return ErrorNotFind
}func main() {var e errore = FindUser()log.Println(e)// 尽量少用if e == ErrorNotFind {}
}
目前我们的错误结构体还是非常简单的,如果我们的结构体里面的属性再多几个,很可能就会出现牛头对马嘴情况。
注意:Go 中 struct是值类型,所以==比较,是比较里面每个字段的值是否相等
所以官方为我们提供了 Is 方法的 API,他默认使用==将特定的错误与错误链中的错误进行比较,如果不一样,就会去调用错误实现的Is方法进行比较。
func (this *MyError) Is(target error) bool {log.Println("到这里来了....")if inputE, ok := target.(*MyError); ok {// 注意:== 比较就是比较struct的各字段是否相等,而我们这里用Is,只想比较错误码是否相等//if inputE.Code == this.Code && inputE.Msg == this.Msg {// return true//}if inputE.Code == this.Code {return true}}return false
}func main() {var e errore = FindUser()log.Println(e)// 注意我们定义的时候是var ErrorNotFind = NewMyError(404, "找不到内容")// 但这里传递的msg是ddd,所以用e==NewMyError(404, "ddd")会是falseif errors.Is(e, NewMyError(404, "ddd")) {log.Println("是 ErrorNotFind")}else {log.Println("不是 ErrorNotFind")}
}
首先我们先去实现下 Is 这个方法,随后我们使用 errors.Is 进行比较,你会看到控制台输出了:
$ go run main.go
2024/02/07 14:20:48 404-找不到内容
2024/02/07 14:20:48 到这里来了....
2024/02/07 14:20:48 是 ErrorNotFind
从输出结果不难看出,errors.Is也是先用==进行了比较,发现不相等后,又调用了MyError的Is方法,只比较错误码是否一致,是相等的。
2、Unwrap
这是一个不大常用的API,标准库里面fmt.Errorf就是一个非常典型的使用案例。
场景是什么呢?
我们通常在错误异常的时候,会有给错误加上一些上下文的需求,那在哪里加呢?
就是错误的 Unwrap 方法里面:
func (this *MyError) Unwrap() error {this.Msg = "hello" + this.Msgreturn this
}func main() {var e errore = FindUser()log.Println("最原始的错误:", e)wE := errors.Unwrap(e)log.Println("加了上下文的错误:", wE)
}
然后看下我们的输出结果:
$ go run main.go
2024/02/07 14:30:06 最原始的错误: 404-找不到内容
2024/02/07 14:30:06 加了上下文的错误: 404-hello找不到内容
你会发现,errors.Unwrap 后的错误调用了我们自定义错误的 Unwrap 方法,在我们的 msg 前面加了 hello。
对错误进行编程最常用的两个API就是这两个了,还有一些不大常用的比如 As,大家感兴趣的可以自行去翻阅下资料。
五、总结
Go 的错误处理和其他语言不太一样,如果遵守错误处理的规范,不对错误进行隐藏,写出来的代码一般都是比较健壮的。
于是就难免会出现一个包里面,特别多的错误处理代码,这就是时间和空间的博弈,就看 Go 语言的领路人如何取舍了。
其次每个人对错误的理解和处理思路方式都不太一样。就比如:我们到底是该多使用哨兵错误,还是该少用呢?
但是最通用的还是哨兵错误以及自定义结构体作为错误。
相关文章:
88.Go设计优雅的错误处理
文章目录 导言一、Go 的约定二、简单错误创建1、 errors.New()2、fmt.Errorf() 三、哨兵错误四、对错误进行编程1、优雅的错误处理设计2、与错误有关的的API 五、总结 导言 在 75.错误码设计、实现统一异常处理和封装统一返回结果 中,我们介绍了错误码的设计&#…...
Python4Delphi: Delphi 程序使用 Python 抓取网页
想用程序去抓取一个网页的内容,Delphi 有自己的 HTTP 库。比如 Indy 的 TIdHTTP,或者 TNetHTTPClient。 这里测试一下使用 Python 的 HTTP 库抓取网页,然后把抓取的内容给 Delphi 的程序。 Delphi 程序,界面上拖控件如下&#x…...
编辑器Zed
什么是Zed 官网:https://zed.dev/ Zed 是 Atom 编辑器原作者主导的新项目 —— 一款支持多人协作的代码编辑器,底层采用 Rust,且默认支持 Rust,还自带了 rust-analyzer,主打 “高性能”,颜值也十分在线&a…...
Java的接口
目录 1.接口的概念 2.语法规则 3.接口的使用 4.接口的特性 总结: 5.实现多个接口 6.接口间的继承 1.接口的概念 接口就是公共的行为规范标准,大家在实现时,只要符合规范标准,就可以通用。 在Java中,接口可以看成…...
【计算机网络】计算机软件工程人工智能研究生复试资料整理
1、JAVA 2、计算机网络 3、计算机体系结构 4、数据库 5、计算机租场原理 6、软件工程 7、大数据 8、英文 自我介绍 2. 计算机网络 1. TCP如何解决丢包和乱序? 序列号:TCP所传送的每段数据都有标有序列号,避免乱序问题发送端确认应答、超时重传:解决丢包问题滑动窗口:避免…...
【Network Management】AUTOSAR架构下CanNm User Data详解
目录 前言 正文 1.CanNm user data概念 2.CanNm user data配置 2.1CDD方式访问CanNm user data...
量子算法入门——2.线性代数与复数
参考资料: 【【零基础入门量子计算-第03讲】线性代数初步与复数】 来自b站up:溴锑锑跃迁 建议关注他的更多高质量文章:CSDN:【溴锑锑跃迁】 0. 前言 强烈建议搭配b站原视频进行观看,这只是我当时看的笔记,…...
分别通过select、多进程、多线程实现一个并发服务器
多进程 #include<myhead.h>#define PORT 8888 //端口号 #define IP "192.168.114.74" //IP地址//定义函数处理客户端信息 int deal_cli_msg(int newfd, struct sockaddr_in cin) {//5、收发数据使用newfd完成通信char buf[128] "&qu…...
如何在 emacs 上开始使用 Tree-Sitter (archlinux)
文章目录 如何在emacs上开始使用Tree-Sitter(archlinux) 如何在emacs上开始使用Tree-Sitter(archlinux) 在archlinux上使用比windows上不知道要方便多少倍! $ sudo pacman -S emacs $ sudo pacman -S tree-sitter这里…...
FL Studio2024最新中文版有哪些其新功能特点?
除了之前提到的特点外,FL Studio 21还有以下一些值得注意的特点: 高效的音频处理:FL Studio 21具备高效的音频处理能力,能够实时处理多轨道音频,提供低延迟的音频播放和录制,确保音乐制作过程中的流畅性和实…...
Oracle的学习心得和知识总结(三十二)|Oracle数据库数据库回放功能之论文四翻译及学习
目录结构 注:提前言明 本文借鉴了以下博主、书籍或网站的内容,其列表如下: 1、参考书籍:《Oracle Database SQL Language Reference》 2、参考书籍:《PostgreSQL中文手册》 3、EDB Postgres Advanced Server User Gui…...
系统架构27 - 软件架构设计(6)
基于架构的软件开发方法 基于架构的软件开发方法(ABSD)概述概念与术语开发模型体系结构需求体系结构设计体系结构文档化体系结构复审体系结构实现体系结构的演化 基于架构的软件开发方法(ABSD) 基于体系结构的软件设计 (Architec…...
STM32 cubemx配置DMA+空闲中断接收不定长数据
文章目录 前言一、串口空闲中断二、DMA空闲中断接收不定长数据实现思路三、STM32Cubemx配置DMA空闲中断接收不定长数据四、代码编写总结 前言 本篇文章给大家讲解一下DMA串口空闲中断接收串口不定长数据,之前我们也是讲解过串口接收不定长数据的,那么本…...
Pycharm配置运行selenium教程
一、下载chrome浏览器和同版本的chromedriver chrome测试版版本120.0.6099.109 链接:https://pan.baidu.com/s/1pvFqL0WN8OkqPmURAs83kg?pwdvtsh 提取码:vtsh chromedriver版本120.0.6099.109 链接:https://pan.baidu.com/s/16fWWkrlD5C3J…...
银河麒麟V10开机后黑屏解决方法
情况描述: 单位的国产化电脑采用银河麒麟V10系统,在使用了近两个月时间后,开机到加载桌面那一步无法加载图形化桌面。 原理讲解 Linux本是纯命令行形式的系统,银河麒麟基于Linux中的Ubuntu LTS内核开发,其图形化的品牌…...
【Git版本控制 02】分支管理
目录 一、创建分支 二、切换分支 三、合并分支 四、删除分支 五、合并冲突 六、分支策略 七、bug分支 一、创建分支 # 当前仓库只有 master 一个主分支 # 可通过 git branch 是进行分支管理的命令,可通过不同参数对分支进行查看、创建、删除(base) [rootloc…...
基金分类
一、按基金运作方式分类 (一)封闭式基金 是基金份额总额在期限内固定不变,在期限内不可申购和赎回。 (二)开放式基金 是基金份额总额不固定,在期限内可以申购和赎回。 这里的开放式基金特指传统的开放式基…...
kali系统概述、nmap扫描应用、john破解密码、抓包概述、以太网帧结构、抓包应用、wireshark应用、nginx安全加固、Linux系统加固
目录 kali nmap扫描 使用john破解密码 抓包 封装与解封装 网络层数据包结构 TCP头部结构编辑 UDP头部结构 实施抓包 安全加固 nginx安全 防止缓冲区溢出 Linux加固 kali 实际上它就是一个预安装了很多安全工具的Debian Linux [rootmyhost ~]# kali resetkali …...
Spring Cloud 路由和消息传递 (HTTP 路由)
Spring Cloud 路由 Spring Cloud 路由是指将请求路由到特定服务的机制。Spring Cloud 提供了多种路由机制,包括: Ribbon: 一个基于 HTTP 和 TCP 的客户端负载均衡工具,提供软负载均衡、故障转移等功能。Feign: 一个声明式的 HTTP 客户端&am…...
【PyQt】12-滑块、计数控件
文章目录 前言一、滑块控件 QSlider运行结果 二、计数器控件 QSpinBox运行结果 总结 前言 1、滑块控件 2、计数控件 一、滑块控件 QSlider #Author :susocool #Creattime:2024/2/15 #FileName:28-滑块控件 #Description: 通过滑块选择字体大小 import sys from PyQ…...
Linux 文件类型,目录与路径,文件与目录管理
文件类型 后面的字符表示文件类型标志 普通文件:-(纯文本文件,二进制文件,数据格式文件) 如文本文件、图片、程序文件等。 目录文件:d(directory) 用来存放其他文件或子目录。 设备…...
微软PowerBI考试 PL300-选择 Power BI 模型框架【附练习数据】
微软PowerBI考试 PL300-选择 Power BI 模型框架 20 多年来,Microsoft 持续对企业商业智能 (BI) 进行大量投资。 Azure Analysis Services (AAS) 和 SQL Server Analysis Services (SSAS) 基于无数企业使用的成熟的 BI 数据建模技术。 同样的技术也是 Power BI 数据…...
Java入门学习详细版(一)
大家好,Java 学习是一个系统学习的过程,核心原则就是“理论 实践 坚持”,并且需循序渐进,不可过于着急,本篇文章推出的这份详细入门学习资料将带大家从零基础开始,逐步掌握 Java 的核心概念和编程技能。 …...
Python Ovito统计金刚石结构数量
大家好,我是小马老师。 本文介绍python ovito方法统计金刚石结构的方法。 Ovito Identify diamond structure命令可以识别和统计金刚石结构,但是无法直接输出结构的变化情况。 本文使用python调用ovito包的方法,可以持续统计各步的金刚石结构,具体代码如下: from ovito…...
DingDing机器人群消息推送
文章目录 1 新建机器人2 API文档说明3 代码编写 1 新建机器人 点击群设置 下滑到群管理的机器人,点击进入 添加机器人 选择自定义Webhook服务 点击添加 设置安全设置,详见说明文档 成功后,记录Webhook 2 API文档说明 点击设置说明 查看自…...
Webpack性能优化:构建速度与体积优化策略
一、构建速度优化 1、升级Webpack和Node.js 优化效果:Webpack 4比Webpack 3构建时间降低60%-98%。原因: V8引擎优化(for of替代forEach、Map/Set替代Object)。默认使用更快的md4哈希算法。AST直接从Loa…...
【Veristand】Veristand环境安装教程-Linux RT / Windows
首先声明,此教程是针对Simulink编译模型并导入Veristand中编写的,同时需要注意的是老用户编译可能用的是Veristand Model Framework,那个是历史版本,且NI不会再维护,新版本编译支持为VeriStand Model Generation Suppo…...
【堆垛策略】设计方法
堆垛策略的设计是积木堆叠系统的核心,直接影响堆叠的稳定性、效率和容错能力。以下是分层次的堆垛策略设计方法,涵盖基础规则、优化算法和容错机制: 1. 基础堆垛规则 (1) 物理稳定性优先 重心原则: 大尺寸/重量积木在下…...
消防一体化安全管控平台:构建消防“一张图”和APP统一管理
在城市的某个角落,一场突如其来的火灾打破了平静。熊熊烈火迅速蔓延,滚滚浓烟弥漫开来,周围群众的生命财产安全受到严重威胁。就在这千钧一发之际,消防救援队伍迅速行动,而豪越科技消防一体化安全管控平台构建的消防“…...
Python环境安装与虚拟环境配置详解
本文档旨在为Python开发者提供一站式的环境安装与虚拟环境配置指南,适用于Windows、macOS和Linux系统。无论你是初学者还是有经验的开发者,都能在此找到适合自己的环境搭建方法和常见问题的解决方案。 快速开始 一分钟快速安装与虚拟环境配置 # macOS/…...
