88.Go设计优雅的错误处理

文章目录

  • 导言
  • 一、Go 的约定
  • 二、简单错误创建
    • 1、 errors.New()
    • 2、fmt.Errorf()
  • 三、哨兵错误
  • 四、对错误进行编程
    • 1、优雅的错误处理设计
    • 2、与错误有关的的API
  • 五、总结

导言

在 75.错误码设计、实现统一异常处理和封装统一返回结果 中,我们介绍了错误码的设计,包括错误码的分段式含义。本篇文章,我们将结合错误码以及错误msg介绍一下Go中优雅的错误处理是如何设计的。共有两种常见方式:

  1. 哨兵错误
  2. 定义结构体(至少包含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也是先用==进行了比较,发现不相等后,又调用了MyErrorIs方法,只比较错误码是否一致,是相等的。

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 语言的领路人如何取舍了。

其次每个人对错误的理解和处理思路方式都不太一样。就比如:我们到底是该多使用哨兵错误,还是该少用呢?

但是最通用的还是哨兵错误以及自定义结构体作为错误。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/679938.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

矩阵在计算机图像处理中的应用

矩阵在计算机图像处理中是非常核心的概念&#xff0c;因为它们为表示和操作图像数据提供了一种非常方便和强大的方式。以下是矩阵在计算机图像处理中的一些关键作用&#xff1a; 图像表示&#xff1a;在计算机中&#xff0c;图像通常被表示为像素矩阵&#xff0c;也就是二维数组…

假期day5

TCP UDP区别 共同点&#xff1a;都是属于传输层的协议 TCP&#xff1a;稳定。面向连接的&#xff0c;有可靠的数据传输服务。传输过程中数据无误&#xff0c;无丢失&#xff0c;无失序&#xff0c;无重复。传输效率低&#xff0c;耗费资源多。数据收发不同步&#xff0c;有沾…

【JavaScript】变量声明和赋值

文章目录 变量的声明1. 使用 var2. 使用 let3. 使用 const 变量的赋值变量的作用域最佳实践总结 在 JavaScript 中&#xff0c; 变量是我们存储和操作数据的关键。本篇博客将讨论 JavaScript 中的变量声明和赋值&#xff0c;涵盖不同的声明关键字、变量的作用域以及一些最佳实…

从源码学习final的使用

从源码学习final的使用 final的作用 ​ final字面意思&#xff0c;意为最终的、不可变的。在Java中&#xff0c;final可以用来修饰类、方法和变量&#xff0c;可以分别起到不同的作用。 final修饰类&#xff1a;表示该类不可以被继承&#xff1b;final修饰方法&#xff1a;表…

Netty Review - NioEventLoopGroup源码解析

文章目录 概述类继承关系源码分析小结 概述 EventLoopGroup bossGroup new NioEventLoopGroup(1); EventLoopGroup workerGroup new NioEventLoopGroup();这段代码是在使用Netty框架时常见的用法&#xff0c;用于创建两个不同的EventLoopGroup实例&#xff0c;一个用于处理连…

Educational Codeforces Round 1(远古edu计划)

A. 高斯求和&#xff0c;如果2次幂小于n就减2次 #include<bits/stdc.h> #define INF 1e9 using namespace std; typedef long long ll; const int N2e59; int a[N]; int f[N],vis[N]; ll sn(ll a1,ll an,ll num){return (a1an)*num/2;} void init(){f[1]1;for(int i2;i…

【Linux】内核中的链表

&#x1f525;博客主页&#xff1a;PannLZ &#x1f38b;系列专栏&#xff1a;《Linux系统之路》 &#x1f94a;不要让自己再留有遗憾&#xff0c;加油吧&#xff01; 文章目录 链表1.创建和初始化2.创建节点3.添加节点4.删除节点5.遍历 链表 内核开发者只实现了循环双链表&am…

Android 9.0 禁用adb install 安装app功能

1.前言 在9.0的系统产品定制化开发中,在进行一些定制开发中,对于一些app需要通过属性来控制禁止安装,比如adb install也不允许安装,所以就需要 熟悉adb install的安装流程,然后来禁用adb install安装功能,接下来分析下adb 下的安装流程 2.禁用adb install 安装app功能的…

1. pick gtk dll 程序的制作

文章目录 前言预览细节要点初始窗口尺寸提示音快速提示信息对话框AlertDialog鼠标移入移出事件布局与父子控件关系图片 后续源码及资源 前言 在之前的打包测试中我提到了需要一个挑选dll的程序于是我打算用Gtk来制作这个程序 预览 细节要点 初始窗口尺寸 只有主窗口有set_d…

unity显示图片

目录 创建c#脚本 自己创建gui组件&#xff1a; 入门教程&#xff1a; 读取图片&#xff1a; Unity读取图片并显示到UI中 显示双目相机&#xff0c;可以跑通 unity3d显示图片 参考教程&#xff0c;GameObject Unity UGUI的Image&#xff08;图片&#xff09;组件的介绍及…

基于无线传感器网络的LC-DANSE波束形成算法matlab仿真

目录 1.程序功能描述 2.测试软件版本以及运行结果展示 3.核心程序 4.本算法原理 4.1LC-DANSE算法原理 4.2 LCMV算法原理 5.完整程序 1.程序功能描述 在无线传感器网络中&#xff0c;通过MATLAB对比LC-DANSE波束形成算法和LCMV波束形成算法。对比SNR&#xff0c;mse等指标…

Day45- 动态规划part13

一、最长递增子序列 题目一&#xff1a;300. 最长递增子序列​​​​​​​ ​​​​​​​300. 最长递增子序列 给你一个整数数组 nums &#xff0c;找到其中最长严格递增子序列的长度。 子序列 是由数组派生而来的序列&#xff0c;删除&#xff08;或不删除&#xff09;数…

Java中的IO介绍

本章内容 一 、File概念 File可以代表一个目录或者一个文件&#xff0c;并不能代表文件的内容 文件和流的区别&#xff1a;File关注的是文件本身的特征&#xff0c;如名称、路径、修改时间、大小。 流关注的是文件的内容。 二、File基本的操作 常见构造方法 | File(String p…

以用户为中心,酷开科技荣获“消费者服务之星”

在企业顺应消费升级的道路中&#xff0c;企业自身不仅要着力强化对于消费者服务意识的提升&#xff0c;并且要树立诚信自律的行业示范带头作用&#xff0c;助力消费环境稳中向好&#xff0c;不断满足人民群众对美好生活的期待。企业的发展需要消费者的认可&#xff0c;酷开科技…

算法刷题框架

前言&#xff1a;最近积累了一些算法题量&#xff0c;正在刷东神的算法笔记&#xff0c;监督自己记录下读后启发&#xff0c;顺便帮助道友们阅读 数据结构 这一部分老生常谈&#xff0c;数据的存储方式只有顺序存储和链式存储。 最基本的数组和链表对应这两者&#xff0c;栈…

常见的单片机及其功能

在当今电子技术快速发展的时代&#xff0c;单片机作为核心组件&#xff0c;在各类电子项目和产品中扮演着至关重要的角色。它们的应用范围从简单的家用电器控制到复杂的工业自动化系统&#xff0c;几乎无处不在。接下来&#xff0c;我们将以轻松的语言&#xff0c;探讨几种广泛…

放大器设计

目录 简介单阶段放大器:低噪声放大器例题例题2例题3简介 放大器能够放大信号,是电路设计中不可或缺的一种重要软件。根据应用和结构的不同,可以将放大器分为三类。 小信号:设计目标是在输入输出匹配条件下,获取一个特定的传输增益,对输出信号的功率没有要求。低噪声:相…

Spring 如何解决循环依赖?Spring三级缓存

什么是循环依赖 说白是一个或多个对象实例之间存在直接或间接的依赖关系&#xff0c;这种依赖关系构成了构成一个环形调用。 自己依赖自己 两个对象间的依赖关系 多个对象间的依赖关系 Spring出现循环依赖的场景 单例的setter注入 Service public class A {Resourceprivate…

C# 线程与线程池的使用方法、注意事项

在C#中&#xff0c;线程和线程池是两种用于实现多线程编程的方式。线程用于执行并发任务&#xff0c;而线程池提供了一种更有效率的方式来管理和复用线程资源。 C# 线程&#xff08;System.Threading.Thread&#xff09; 创建和启动线程&#xff1a; Thread thread new Thre…

AI算法工程师-非leetcode题目总结

AI算法工程师-非leetcode题目总结 除了Leetcode你还需要这些实现nms旋转矩形IOU手动实现BN手动实现CONV实现CrossEntropyLoss 除了Leetcode你还需要这些 希望大家留言&#xff0c;我可以进行补充。持续更新~~~ 实现nms import numpy as np def nms(dets, threshold):x1 dets…