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…...

MPNet:旋转机械轻量化故障诊断模型详解python代码复现
目录 一、问题背景与挑战 二、MPNet核心架构 2.1 多分支特征融合模块(MBFM) 2.2 残差注意力金字塔模块(RAPM) 2.2.1 空间金字塔注意力(SPA) 2.2.2 金字塔残差块(PRBlock) 2.3 分类器设计 三、关键技术突破 3.1 多尺度特征融合 3.2 轻量化设计策略 3.3 抗噪声…...
树莓派超全系列教程文档--(62)使用rpicam-app通过网络流式传输视频
使用rpicam-app通过网络流式传输视频 使用 rpicam-app 通过网络流式传输视频UDPTCPRTSPlibavGStreamerRTPlibcamerasrc GStreamer 元素 文章来源: http://raspberry.dns8844.cn/documentation 原文网址 使用 rpicam-app 通过网络流式传输视频 本节介绍来自 rpica…...
Objective-C常用命名规范总结
【OC】常用命名规范总结 文章目录 【OC】常用命名规范总结1.类名(Class Name)2.协议名(Protocol Name)3.方法名(Method Name)4.属性名(Property Name)5.局部变量/实例变量(Local / Instance Variables&…...
工程地质软件市场:发展现状、趋势与策略建议
一、引言 在工程建设领域,准确把握地质条件是确保项目顺利推进和安全运营的关键。工程地质软件作为处理、分析、模拟和展示工程地质数据的重要工具,正发挥着日益重要的作用。它凭借强大的数据处理能力、三维建模功能、空间分析工具和可视化展示手段&…...

Python爬虫(一):爬虫伪装
一、网站防爬机制概述 在当今互联网环境中,具有一定规模或盈利性质的网站几乎都实施了各种防爬措施。这些措施主要分为两大类: 身份验证机制:直接将未经授权的爬虫阻挡在外反爬技术体系:通过各种技术手段增加爬虫获取数据的难度…...
是否存在路径(FIFOBB算法)
题目描述 一个具有 n 个顶点e条边的无向图,该图顶点的编号依次为0到n-1且不存在顶点与自身相连的边。请使用FIFOBB算法编写程序,确定是否存在从顶点 source到顶点 destination的路径。 输入 第一行两个整数,分别表示n 和 e 的值(1…...

OPenCV CUDA模块图像处理-----对图像执行 均值漂移滤波(Mean Shift Filtering)函数meanShiftFiltering()
操作系统:ubuntu22.04 OpenCV版本:OpenCV4.9 IDE:Visual Studio Code 编程语言:C11 算法描述 在 GPU 上对图像执行 均值漂移滤波(Mean Shift Filtering),用于图像分割或平滑处理。 该函数将输入图像中的…...

OPENCV形态学基础之二腐蚀
一.腐蚀的原理 (图1) 数学表达式:dst(x,y) erode(src(x,y)) min(x,y)src(xx,yy) 腐蚀也是图像形态学的基本功能之一,腐蚀跟膨胀属于反向操作,膨胀是把图像图像变大,而腐蚀就是把图像变小。腐蚀后的图像变小变暗淡。 腐蚀…...
Leetcode33( 搜索旋转排序数组)
题目表述 整数数组 nums 按升序排列,数组中的值 互不相同 。 在传递给函数之前,nums 在预先未知的某个下标 k(0 < k < nums.length)上进行了 旋转,使数组变为 [nums[k], nums[k1], …, nums[n-1], nums[0], nu…...
深入浅出WebGL:在浏览器中解锁3D世界的魔法钥匙
WebGL:在浏览器中解锁3D世界的魔法钥匙 引言:网页的边界正在消失 在数字化浪潮的推动下,网页早已不再是静态信息的展示窗口。如今,我们可以在浏览器中体验逼真的3D游戏、交互式数据可视化、虚拟实验室,甚至沉浸式的V…...