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…...
生成xcframework
打包 XCFramework 的方法 XCFramework 是苹果推出的一种多平台二进制分发格式,可以包含多个架构和平台的代码。打包 XCFramework 通常用于分发库或框架。 使用 Xcode 命令行工具打包 通过 xcodebuild 命令可以打包 XCFramework。确保项目已经配置好需要支持的平台…...
Vue记事本应用实现教程
文章目录 1. 项目介绍2. 开发环境准备3. 设计应用界面4. 创建Vue实例和数据模型5. 实现记事本功能5.1 添加新记事项5.2 删除记事项5.3 清空所有记事 6. 添加样式7. 功能扩展:显示创建时间8. 功能扩展:记事项搜索9. 完整代码10. Vue知识点解析10.1 数据绑…...

CTF show Web 红包题第六弹
提示 1.不是SQL注入 2.需要找关键源码 思路 进入页面发现是一个登录框,很难让人不联想到SQL注入,但提示都说了不是SQL注入,所以就不往这方面想了 先查看一下网页源码,发现一段JavaScript代码,有一个关键类ctfs…...
java_网络服务相关_gateway_nacos_feign区别联系
1. spring-cloud-starter-gateway 作用:作为微服务架构的网关,统一入口,处理所有外部请求。 核心能力: 路由转发(基于路径、服务名等)过滤器(鉴权、限流、日志、Header 处理)支持负…...

【HarmonyOS 5.0】DevEco Testing:鸿蒙应用质量保障的终极武器
——全方位测试解决方案与代码实战 一、工具定位与核心能力 DevEco Testing是HarmonyOS官方推出的一体化测试平台,覆盖应用全生命周期测试需求,主要提供五大核心能力: 测试类型检测目标关键指标功能体验基…...
java调用dll出现unsatisfiedLinkError以及JNA和JNI的区别
UnsatisfiedLinkError 在对接硬件设备中,我们会遇到使用 java 调用 dll文件 的情况,此时大概率出现UnsatisfiedLinkError链接错误,原因可能有如下几种 类名错误包名错误方法名参数错误使用 JNI 协议调用,结果 dll 未实现 JNI 协…...

selenium学习实战【Python爬虫】
selenium学习实战【Python爬虫】 文章目录 selenium学习实战【Python爬虫】一、声明二、学习目标三、安装依赖3.1 安装selenium库3.2 安装浏览器驱动3.2.1 查看Edge版本3.2.2 驱动安装 四、代码讲解4.1 配置浏览器4.2 加载更多4.3 寻找内容4.4 完整代码 五、报告文件爬取5.1 提…...

基于IDIG-GAN的小样本电机轴承故障诊断
目录 🔍 核心问题 一、IDIG-GAN模型原理 1. 整体架构 2. 核心创新点 (1) 梯度归一化(Gradient Normalization) (2) 判别器梯度间隙正则化(Discriminator Gradient Gap Regularization) (3) 自注意力机制(Self-Attention) 3. 完整损失函数 二…...
MySQL JOIN 表过多的优化思路
当 MySQL 查询涉及大量表 JOIN 时,性能会显著下降。以下是优化思路和简易实现方法: 一、核心优化思路 减少 JOIN 数量 数据冗余:添加必要的冗余字段(如订单表直接存储用户名)合并表:将频繁关联的小表合并成…...

[大语言模型]在个人电脑上部署ollama 并进行管理,最后配置AI程序开发助手.
ollama官网: 下载 https://ollama.com/ 安装 查看可以使用的模型 https://ollama.com/search 例如 https://ollama.com/library/deepseek-r1/tags # deepseek-r1:7bollama pull deepseek-r1:7b改token数量为409622 16384 ollama命令说明 ollama serve #:…...