Go语言的100个错误使用场景(48-54)|错误管理

news/发布时间2024/5/15 19:57:20

前言

大家好,这里是白泽。**《Go语言的100个错误以及如何避免》**是最近朋友推荐我阅读的书籍,我初步浏览之后,大为惊喜。就像这书中第一章的标题说到的:“Go: Simple to learn but hard to master”,整本书通过分析100个错误使用 Go 语言的场景,带你深入理解 Go 语言。

我的愿景是以这套文章,在保持权威性的基础上,脱离对原文的依赖,对这100个场景进行篇幅合适的中文讲解。所涉内容较多,总计约 8w 字,这是该系列的第六篇文章,对应书中第48-54个错误场景。

🌟 当然,如果您是一位 Go 学习的新手,您可以在我开源的学习仓库中,找到针对**《Go 程序设计语言》**英文书籍的配套笔记,其他所有文章也会整理收集在其中。

📺 B站:白泽talk,公众号【白泽talk】,聊天交流群:622383022,原书电子版可以加群获取。

前文链接:

  • 《Go语言的100个错误使用场景(1-10)|代码和项目组织》
  • 《Go语言的100个错误使用场景(11-20)|项目组织和数据类型》
  • 《Go语言的100个错误使用场景(21-29)|数据类型》
  • 《Go语言的100个错误使用场景(30-40)|数据类型与字符串使用》
  • 《Go语言的100个错误使用场景(40-47)|字符串&函数&方法》

7. 错误管理

🌟 章节概述:

  • 懂得何时使用 panic
  • 懂得何时包裹错误
  • 高效对比 error 类型和值(Go1.13)
  • 地道地处理 error
  • 懂得何时可以忽略 error
  • 在 defer 调用中处理 error

7.1 panicking(#48)

panic 使用示例:

func main() {defer func() {if r := recover(); r != nil {fmt.Println("recover", r)}}()f()fmt.Println("c")
}func f() {fmt.Println("a")panic("foo")fmt.Println("b")
}
// 结果
a
recover foo

在触发 panic 之后,结束当前函数的执行,并且跳出函数调用栈:main(),在 main 函数中,在 return 之前,panic 被 recover 捕获。

⚠️ 注意:recover 必须声明在 defer 函数中,因为 defer 在程序 panic 之后,依旧会执行。

推荐使用 panic 的场景:

  1. 当发生系统错误,如返回的 HTTP 状态码 < 100 或者 > 999,此时意味着系统必然出错了。
  2. 当必要的依赖无法获取,且影响程序的功能运行时。

7.2 不清楚何时应该包裹一个 error(#49)

🌟 可能需要包裹一个 error 的场景:

  • 为一个 error 添加额外的上下文信息
// 在 Go1.13之后,可以通过 %w 实现
if err != nil {return fmt.Errorf("bar failed: %w", err)
}

此时创建的 error 本质是 wrapError 这个结构体,它有两个字段,msg 记录上述格式化的内容,而 err 字段保存一份原来的 err。

image-20240219225621301

这种场景的好处就是可以额外增加一些上下文信息,但是当解析错误的一端获取这个 error 之后,需要对应去 unwrap 这个错误,一定程度上也增加了耦合度,错误处理部分代码相当于与具体的错误有绑定关系,如果换一个错误可能需要编写其他的错误处理逻辑。

// 通过 %v 而不是 %w
if err != nil {return fmt.Errorf("bar failed: %w", err)
}

此时创建的 error 本质是一个单层的新的错误,它的内容就是格式化后的字符串,原先的 error 不存在了,好处就是无需额外 unwrap 一次。

image-20240219230200919

  • 将一个 error 标记为一个特定类型的错误。
type BarError struct {Err error
}func (b BarError) Error() string {return "bar failed:" + b.Err.Error()
}
---------------------------------------------
if err != nil {return BarError{Err: err}
}

这种情况下需要自定义错误类型,好处是比较灵活,但是缺点是比较麻烦,要针对不同的类型的错误创建不同的结构体。

7.3 检查错误类型不够精确(#50)

场景假设:编写一个 HTTP 服务提供根据 ID 进行查询数量的功能,当 ID 传入格式错误时 HTTP 响应状态码 400,当数据库服务不可用时,响应状态码 503,根据这个场景,以下将提供几种错误类型检查方式。

自定义错误类型:

type transientError struct {err error
}func (t transientError) Error() string {return fmt.Sprintf("transient error: %v": t.err)
}

错误类型检查方式一:

// 根据 ID 获取数量
func getTransactionAmount(transcationID string) (float32, error) {if len(transcationID) != 5 {return 0, fmt.Errorf("id is invaild: %s", transcationID)}amount, err := getTranscationAmountFromDB(transcationID)if err != nil {return 0, transientError{Err: err}}return amount, nil
}
// http 服务
func handler(w http.ResponseWriter, r *http.Request) {transcationID := r.URL.Query().Get("transcationID")amount, err := getTransactionAmount(transcationID)if err != nil {switch err := err.(type) {case transientError:http.Error(w, err.Error(), http.StatusServiceUnavailable)default:http.Error(w, err.Error(), http.StatusBadRequest)}return}// 返回正确响应
}

这种场景下,根据断言检验返回的错误类型,通过 switch 分条件返回 400 和 503。

错误类型检查方式二:

// 假设 transientError 类型的错误将从 getTranscationAmountFromDB 返回
func getTranscationAmountFromDB(transcationID)float32, error{// ...if err != nil {return 0, transientError{err: err}}// ...
}// 根据 ID 获取数量
func getTransactionAmount(transcationID string) (float32, error) {if len(transcationID) != 5 {return 0, fmt.Errorf("id is invaild: %s", transcationID)}amount, err := getTranscationAmountFromDB(transcationID)if err != nil {return 0, fmt.Errorf("failed to get transcation %s: %w", transcationID, err)}return amount, nil
}
// http 服务
func handler(w http.ResponseWriter, r *http.Request) {transcationID := r.URL.Query().Get("transcationID")amount, err := getTransactionAmount(transcationID)if err != nil {if errors.As(err, &transienError{}) {http.Error(w, err.Error(),http.StatusServiceUnavailable)} else {http.Error(w, err.Error(), http.StatusBadRequest)}return}// 返回正确响应
}

当 transientError 类型的错误将从 getTranscationAmountFromDB 返回,getTransactionAmount 函数返回的 warpError 结构将包裹 transientError,此时直接使用方式一中的断言将无法检测出被包裹的错误。

需要使用 Go1.13 提供的 ``errors.As(err error, target interface{}) bool方法,这个方法可以不断调用 warpError 结构的Unwrap方法,直到遇到 error 包装链中存在errors.As()` 函数第二个指针参数对应的错误类型,返回 true,并将错误存放在 target 变量中。

🌟 errors.As() 通常用于处理多种可能的错误类型,以便根据不同类型的错误执行不同逻辑,本质用于提取目标类型的错误。

7.4 检查错误值不够精确(#51)

示例代码:

err := query()
if err != nil {// 这里如果使用 == 比较两个类型错误,则如果遇到 wrapError,将永远为 falseif errors.Is(err, sql.ErrNoRows) {// ...} else {// ...}
}

假设 err 可能是一个 warpError 结构,内部包裹着一个提前声明的错误:sql.ErrNoRows,但也有可能返回的 err 直接就是这个 sql.ErrNoRows,此时使用 errors.Is() 方法可以通过 unwrap 的方式,判断错误链上是否有某个具体的错误。

🌟 errors.Is() 本质用于判断目标错误类型是否存在。

7.5 一个错误处理两次(#52)

错误示例:

func getRoute(srcLat, srcLng, dstLat, dstLng float32) (Route, error) {err := validateCoordinates(srcLat, srcLng)if err != nil {log.Println("failed to validate source coordinates")return Route{}, err}err := validateCoordinates(dstLat, dstLng)if err != nil {log.Println("failed to validate source coordinates")return Route{}, err}return getRoute(srcLat, srcLng, dstLat, dstLng), nil
}func validateCoordinates(lat, lng float32) error {if lat > 90.0 || lat < -90.0 {log.Printf("invalid latitude: %f", lat)return fmt.Errorf("invalid latitude: %f", lat)}if lng > 180.0 || lng < -180.0 {log.Printf("invalid longitude: %f", lng)return fmt.Errorf("invalid longitude: %f", lng)}return nil
}

这种情况下存在两个问题:

  1. 错误发生将打印两条日志,在高并发环境下,日志会出现乱序。
  2. 打印错误日志与 return 错误是两种处理错误的方式,只需要选择一种即可,打印了日志已经是处理了错误了。

修正示例1.0版本:

func getRoute(srcLat, srcLng, dstLat, dstLng float32) (Route, error) {err := validateCoordinates(srcLat, srcLng)if err != nil {return Route{}, err}err := validateCoordinates(dstLat, dstLng)if err != nil {return Route{}, err}return getRoute(srcLat, srcLng, dstLat, dstLng), nil
}func validateCoordinates(lat, lng float32) error {if lat > 90.0 || lat < -90.0 {return fmt.Errorf("invalid latitude: %f", lat)}if lng > 180.0 || lng < -180.0 {return fmt.Errorf("invalid longitude: %f", lng)}return nil
}

此时放弃了错误日志打印,只保留了 return 的处理方式。但此时有一个问题,如果发生问题,getRoute 函数最后只会包含 invalid latitude: xxx 这样类似的错误信息,但是不知道是归属 src 还是 tar,因为原本第二条日志虽然会造成乱序,但本质还是提供了额外的错误信息的,这部分不能直接省略。

修正示例2.0版本:

func getRoute(srcLat, srcLng, dstLat, dstLng float32) (Route, error) {err := validateCoordinates(srcLat, srcLng)if err != nil {return Route{}, fmt.Errorf("failed to validate source coordinates: %w", err)}err := validateCoordinates(dstLat, dstLng)if err != nil {return Route{}, fmt.Errorf("failed to validate target coordinates: %w", err)}return getRoute(srcLat, srcLng, dstLat, dstLng), nil
}func validateCoordinates(lat, lng float32) error {if lat > 90.0 || lat < -90.0 {return fmt.Errorf("invalid latitude: %f", lat)}if lng > 180.0 || lng < -180.0 {return fmt.Errorf("invalid longitude: %f", lng)}return nil
}

通过 fmt.Errorf() 的方式,将需要添加的上下文信息追加上去。

7.6 不处理错误(#53)

示例代码:

// At-most once delivery
// Hence, it's accepted to miss some of them in case of errors.
_ = notify()

如果确实可以忽略错误,则通过短下划线方式声明,而不是直接放弃返回值,同时配合注释说明原因。

7.7 不处理 defer 错误(#54)

场景:使用 sql.DB 数据库连接池查询数据库

错误示例1:

const query = "..."func getBalance(db *sql.DB, clientID string) (float32, error) {rows, err := db.Query(query, clientID)if err != nil {return 0, err}defer rows.Close()if rows.Next() {err := rows.Scan(&balance)if err != nil {return 0, err}return balance, nil}// ...
}type Closer interface {Close() error
}

此时 rows.Close() 的调用可能会发生错误,表示连接池关闭失败,但是示例中没有处理这部分错误。

错误示例2:

func getBalance(db *sql.DB, clientID string) (float32, error) {rows, err := db.Query(query, clientID)if err != nil {return 0, err}defer func() {err = rows.Close()}()if rows.Next() {err := rows.Scan(&balance)if err != nil {return 0, err}return balance, nil}// ...
}

通过赋值的方式,将 rows.Close() 导致的错误传递给外部变量 err,但是这导致了一个新的问题,当下文 rows.Next() 发生错误的时候,无论情况如何,err 的内容最终都将被 rows.Close() 的执行结果覆盖,如果 rows.Next() 报错,但是 rows.Close() 正常关闭,则 err 最终为 nil。

🌟 修正示例:

defer func() {closeErr := rows.Close()if err != nil {if closeErr != nil {log.Printf("failed to close rows: %v", err)}return}err = closeErr
}
  • 当执行 rows.Close() 之前,如果 err 已经不是 nil
    • 如果 rows.Close() 报错,则打印一条日志
    • 如果 rows.Close() 关闭成功,则直接返回业务相关的 err
  • 当执行 rows.Close() 之前,如果 err 是 nil,则将 closeErr 赋值给 err,无论 closeErr 是否为 nil

🌟 上述的逻辑执行核心就是优先返回 getBalance() 内业务逻辑涉及到的错误,没有错误再考虑返回关闭连接池导致的错误。

小结

你已完成《Go语言的100个错误》全书学习进度54%,欢迎追更。

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

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

相关文章

高效的FTP替代产品,如何解决FTP文件传输存在的弊端?

FTP最初并不是为IP网络设计的&#xff0c;而是在ARPANET&#xff08;Advanced Research Projects Agency Network&#xff0c;美国国防部高级研究计划署网络&#xff09;中作为计算机间文件传输的协议。1971年&#xff0c;Abhay Bhushan提出了FTP的第一个RFC&#xff08;Reques…

Out of memory,realloc failed

git config --global http.postBuffer 1048576000

SpringBoot整合GateWay(详细配置)

前言 在Spring Boot中整合Spring Cloud Gateway是一个常见的需求&#xff0c;尤其是当需要构建一个微服务架构的应用程序时。Spring Cloud Gateway是Spring Cloud生态系统中的一个项目&#xff0c;它提供了一个API网关&#xff0c;用于处理服务之间的请求路由、安全、监控和限流…

PyCharm 主题和字体 (Scheme Editor Font)

PyCharm 主题和字体 [Scheme & Editor Font] References Scheme & Editor Font File -> Settings -> Editor -> Colors & Fonts -> Font Show only monospaced fonts&#xff1a; 只显示等宽字体。编程时使用等宽字体效果较好。 References [1] Yon…

世界顶级名校计算机专业,都在用哪些书当教材?

清华、北大、MIT、CMU、斯坦福的学霸们在新学期里要学什么&#xff1f;今天我们来盘点一下那些世界名校计算机专业采用的教材。 欢迎来到英杰社区&#xff1a; https://bbs.csdn.net/topics/617804998 欢迎来到阿Q社区&#xff1a; https://bbs.csdn.net/topics/617897397 作者…

Linux-文件文件夹相关命令

目录 常见命令 1. 创建空目录&#xff1a;mkdir 文件夹名 2. 删除空目录&#xff1a;rmdir 文件夹名 3. 创建多级目录&#xff1a;mkdir -p 123/abc 4. 删除非空文件 rm -rf 文件夹名 5. 创建文件&#xff1a; touch 文件名.后缀 / vi 文件名.后缀 6. 删除文件&#x…

MyBatisPlus常用注解

目录 一、TableName 二、TableId 三、TableField 四、TableLogic 一、TableName 在使用MyBatis-Plus实现基本的CRUD时&#xff0c;我们并没有指定要操作的表&#xff0c;只是在Mapper接口继承BaseMapper时&#xff0c;设置了泛型User&#xff0c;而操作的表为user表 由此得出…

解决Uncaught SyntaxError: Cannot use import statement outside a module(at XXX)报错

报错原因&#xff1a;这个错误通常是因为你正在尝试在一个不支持 ES6 模块语法的环境中使用 import 语句。这可能是因为你的代码是在一个只支持 CommonJS 或 AMD 模块系统的环境中运行的&#xff0c;或者你的代码运行的环境没有正确配置以支持 ES6 模块。如果是在浏览器环境&am…

Day 30 标准IO

文章目录 1.什么是标准IO1.1 概念1.2 特点1.3 操作 2.缓存区3.函数接口3.1 打开文件fopen3.2 关闭文件 fclose3.3 读写文件操作3.3.1 每次读写一个字符&#xff1a;fgetc()、fputc()每次读一个字符fgetc()每次写一个字符fputc()(1)针对文件(2)针对终端feof和ferror 3.3.2 每次一…

Linux常见指令(一)

目录 一、基本指令 1.1ls指令 1.2pwd指令 1.3cd指令 1.4touch指令 1.5mkdir指令 1.6rmdir指令、rm指令 1.7man指令 1.8cp指令 1.9mv指令 1.10cat 一、基本指令 1.1ls指令 语法 &#xff1a; ls [ 选项 ][ 目录或文件 ] 功能&#xff1a;对于目录&#xff0c;该命令…

c++类和对象新手保姆级上手教学(下)

目录 前言&#xff1a; 初始化列表&#xff1a; explicit关键字&#xff1a; static成员&#xff1a; 友元函数&#xff1a; 友元类&#xff1a; 内部类&#xff1a; 匿名对象&#xff1a; 前言&#xff1a; 类和对象下篇中剩余的部分较为简单易理解&#xff0c;认真记住…

Facebook与数字创新:引领社交媒体的数字化革命

在当今数字化时代&#xff0c;社交媒体已经成为了人们日常生活中不可或缺的一部分。而在众多社交媒体平台中&#xff0c;Facebook作为领头羊&#xff0c;一直致力于推动数字创新&#xff0c;引领着社交媒体的数字化革命。本文将探讨Facebook在数字创新方面的表现&#xff0c;以…

AI:129-基于深度学习的极端天气事件预警

🚀点击这里跳转到本专栏,可查阅专栏顶置最新的指南宝典~ 🎉🎊🎉 你的技术旅程将在这里启航! 从基础到实践,深入学习。无论你是初学者还是经验丰富的老手,对于本专栏案例和项目实践都有参考学习意义。 ✨✨✨ 每一个案例都附带有在本地跑过的关键代码,详细讲解供…

爬虫基本库的使用(requests库的详细解析)

注&#xff1a;本文一共4万多字&#xff0c;希望读者能耐心读完&#xff01;&#xff01;&#xff01; 前面,我们了解了urllib库的基本用法&#xff08;爬虫基本库的使用(urllib库的详细解析)-CSDN博客&#xff09;。其中&#xff0c;确实又不方便的地方。例如处理网页验证…

AI算法初识之分类汇总

一、背景 AI算法的分类方式多种多样&#xff0c;可以根据不同的学习机制、功能用途以及模型结构进行划分。以下是一些主要的分类方式及相应的代表性算法&#xff1a; 1. 按照学习类型 - **监督学习**&#xff1a; - 线性回归&#xff08;Linear Regression&#xff09; …

Python 实现 OBV 指标计算:股票技术分析的利器系列(7)

Python 实现 OBV 指标计算&#xff1a;股票技术分析的利器系列&#xff08;7&#xff09; 介绍算法解释 代码rolling函数介绍核心代码计算 VA 列计算 OBV 列计算 MAOBV 完整代码 介绍 OBV 指标是“On-Balance Volume”的缩写&#xff0c;意为“量价平衡指标”。它是一种用于衡…

【鸿蒙 HarmonyOS 4.0】TypeScript开发语言

一、背景 HarmonyOS 应用的主要开发语言是 ArkTS&#xff0c;它由 TypeScript&#xff08;简称TS&#xff09;扩展而来&#xff0c;在继承TypeScript语法的基础上进行了一系列优化&#xff0c;使开发者能够以更简洁、更自然的方式开发应用。值得注意的是&#xff0c;TypeScrip…

C++ Primer 笔记(总结,摘要,概括)——第5章 语句

目录 5.1 简单语句 5.2 语句作用域 5.3 条件语句 5.3.1 if语句 5.3.2 switch语句 5.4 迭代语句 5.4.1 while语句 5.4.2 传统的for语句 5.4.3 范围for语句 5.4.4 do while语句 5.5 跳转语句 5.5.1 break语句 5.5.2 continue语句 5.5.3 goto语句 5.6 try语句块和异常处理 5…

【漏洞复现-通达OA】通达OA share身份认证绕过漏洞

一、漏洞简介 通达OA(Office Anywhere网络智能办公系统)是中国通达公司的一套协同办公自动化软件。通达OA /share/handle.php存在一个认证绕过漏洞,利用该漏洞可以实现任意用户登录。攻击者可以通过构造恶意攻击代码,成功登录系统管理员账户,继而在系统后台上传恶意文件控…

发布 rust 源码包 (crates.io)

rust 编程语言的包 (或者 库, library) 叫做 crate, 也就是软件中的一个组件. 一个完整的软件通常由多个 crate 组成, rust 编译器 (rustc) 一次编译一整个 crate, 不同的 crate 可以同时并行编译. rust 官方有一个集中发布开源包的网站 crates.io. 发布在这上面的 crate 可以…
推荐文章