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

前言

大家好,这里是白泽。**《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,将永远为 false
    if 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%,欢迎追更。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

shell脚本实现Mysql分库分表备份

一.数据库的分库分表&#xff1f; 12张图把分库分表讲的明明白白&#xff01;阿里面试&#xff1a;我们为什么要分库分表https://mp.weixin.qq.com/s?__bizMzU0OTE4MzYzMw&mid2247547792&idx2&sn91a10823ceab0cb9db26e22783343deb&chksmfbb1b26eccc63b784879…

基于ORB-SLAM2与YOLOv8剔除动态特征点(三种方法)

基于ORB-SLAM2与YOLOv8剔除动态特征点(三种方法) 写上篇文章时测试过程比较乱&#xff0c;写的时候有些地方有点失误&#xff0c;所以重新写了这篇 本文内容均在RGB-D环境下进行程序测试 本文涉及到的动态特征点剔除速度均是以https://cvg.cit.tum.de/data/datasets/rgbd-dat…

c++ 到底是什么呢。需要学的太多了!

如谷歌&#xff0c;苹果&#xff0c;微软&#xff0c;造硬件的更是如此&#xff0c;如amd的&#xff0c;英传达&#xff0c;英特尔&#xff0c;他们都有C加速库。 C难就难在你如果达到顶峰&#xff0c;像些公司一样&#xff0c;是非常难的。因为这些公司已经垄断了市场。 C编译…

Android 圆环带刻度条进度动画效果实现

效果图 需求是根据传感器做一个重力球效果&#xff0c;先实现了动画后续加上跟传感器联动. 又是摆烂的一天&#xff0c; 尚能呼吸&#xff0c;未来可期啊 View源码 package com.android.circlescalebar.view;import android.content.Context; import android.content.res.Typ…

LabVIEW高效核磁测井仪器多线程优化

LabVIEW高效核磁测井仪器多线程优化 为提高核磁测井仪器的测试效率与性能&#xff0c;开发了基于LabVIEW的多线程优化模型。该研究针对传统的核磁测井仪器软件&#xff0c;在多任务调度测试和并行技术需求上存在的效率不高和资源利用率低的问题&#xff0c;提出了一个多线程优…

智慧公厕管理系统:让城市智慧驿站更加智慧舒适

智慧公厕管理系统是城市智慧驿站中不可或缺的一部分&#xff0c;它通过全方位的信息化解决方案&#xff0c;为公共厕所的使用、运营和管理提供了一种智能化的方式。作为城市智慧驿站的重要组成部分&#xff0c;智慧公厕管理系统发挥着重要的作用&#xff0c;为城市社会民生提供…

数字化转型导师坚鹏:数字政府技术、业务、数据融合发展路径探索

数字政府建设与发展研究 ——技术、业务、数据融合发展路径探索 课程背景&#xff1a; 很多政府存在以下问题&#xff1a; 不清楚数字政府建设内涵 不清楚数字政府建设现状 不清楚数字政府融合路径 课程特色&#xff1a; 有实战案例 有原创观点 有精彩解读 学…

基于Android的大学生足球赛事管理系统的设计与实现

足球是世界范围内广受欢迎的一种体育运动&#xff0c;国内有中超、中甲及大学生联赛等各级别的赛事&#xff0c;中超和中甲基本上都有专业的球队在运营&#xff0c;而大学生联赛属于校园级别的赛事&#xff0c;其重视程度较为有限&#xff0c;使得其信息化水平不高&#xff0c;…

【2024软件测试面试必会技能】python(5):python读取excel数据

python读取excel数据 xlrd参考&#xff1a;https://www.cnblogs.com/dream66/p/12572007.html openpyxl参考&#xff1a;https://www.cnblogs.com/dream66/p/12599627.html xlrd/xlwt模块简介&#xff1a; python操作excel主要用到xlrd和xlwt这两个库&#xff0c;即xlrd是读…

《最新出炉》系列初窥篇-Python+Playwright自动化测试-20-处理鼠标拖拽-下篇

1.简介 上一篇中&#xff0c;宏哥说的宏哥在最后提到网站的反爬虫机制&#xff0c;那么宏哥在自己本地做一个网页&#xff0c;没有那个反爬虫的机制&#xff0c;谷歌浏览器是不是就可以验证成功了&#xff0c;宏哥就想验证一下自己想法&#xff0c;其次有人私信宏哥说是有那种…

嵌入式培训机构四个月实训课程笔记(完整版)-Linux ARM驱动编程第八天-高级驱动framebuffer(物联技术666)

链接&#xff1a;https://pan.baidu.com/s/1cd7LOSAvmPgVRPAyuMX7Fg?pwd1688 提取码&#xff1a;1688 帧缓冲&#xff08;framebuffer&#xff09;设备应用于linux显示技术方面。因为linux的显示平台已经全部基于framebuffer&#xff0c;所以目前在linux环境下开发图形化界面、…

了解JSON的作用及其方法

什么是json JSON&#xff08;JavaScript Object Notation&#xff09;是一种轻量级的数据交换格式采用完全独立编程语言的文本格式存储和表示数据&#xff08;就是字符串&#xff09;。它基于JavaScript语法&#xff0c;但可以被多种编程语言使用和解析。JSON以键值对的形式存…

Stable Diffusion 绘画入门教程(webui)-ControlNet(深度Depth)

上篇文章介绍了线稿约束&#xff0c;这篇文章介绍下深度Depth 文章目录 一、选大模型二、写提示词三、基础参数设置四、启用ControlNet 顾名思义&#xff0c;就是把原图预处理为深度图&#xff0c;而深度图可以区分出图像中各元素的远近关系&#xff0c;那么啥事深度图&#xf…

鸿蒙开源!OpenHarmony——手机的CPU信息应用

1.应用安装步骤 应用下载地址与源码开源如下&#xff1a; CPU_device_information 2.实现功能 完成了开发者手机以下信息的获取 - CPU核心数 - SOC型号 - GPU温度 - 主板温度 - 系统运行时间 - RAM总内存 - RAM可用内存 - RAM空闲内存 - 缓存使用内存 - Swaps交换分区 - 系…

JavaScript在web自动化测试中的应用

&#x1f525; 交流讨论&#xff1a;欢迎加入我们一起学习&#xff01; &#x1f525; 资源分享&#xff1a;耗时200小时精选的「软件测试」资料包 &#x1f525; 教程推荐&#xff1a;火遍全网的《软件测试》教程 &#x1f4e2;欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1…

契约锁与400多家软件厂商实现集成应用

契约锁电子签及印控产品具备200多种功能接口&#xff0c;拥有400多家管理软件厂商集成对接经验&#xff0c;覆盖ERP、OA、业务系统、移动APP应用、低代码平台、BPM、小程序等42种软件类型&#xff0c;在帮助组织落实印章管理制度的同时&#xff0c;按需构建业务电子签场景&…

【计算机网络】一些乱七八糟内容

MAC Media Access Control 用于在局域网&#xff08;LAN&#xff09;或广域网&#xff08;WAN&#xff09;中实现设备自动接入网络 "载波侦听多路访问"(Carrier Sense Multiple Access) CSMA/CD 是CSMA的升级版本&#xff0c;加入了序列号检测机制。 CSMA/CA 是CSM…

旅游分享系列之:福建旅游攻略

旅游分享系列之&#xff1a;福建旅游攻略 一、漳州1.福建土楼2.云水谣3.四菜一汤景点 二、厦门1.园林博览苑2.海上自行车道3.山海步道4.海滩5.闽南菜6.落日 三、泉州1.衙口沙滩2.海上日出3.珞珈寺4.海滩烟花 一、漳州 游玩2个景点&#xff1a;云水谣&#xff0c;四菜一汤可以住…

图形系统开发实战课程:进阶篇(上)——6.图形交互操作:拾取

图形开发学院&#xff5c;GraphAnyWhere 课程名称&#xff1a;图形系统开发实战课程&#xff1a;进阶篇(上)课程章节&#xff1a;“图形交互操作:拾取”原文地址&#xff1a;https://www.graphanywhere.com/graph/advanced/2-6.html 第六章 图形交互操作:拾取 \quad 在图形系统…

Avalonia 初学笔记(2):简单了解与WPF的区别

文章目录 相关链接前言Avalonia相对于WPF的新特性简单介绍ChatGPT推荐Avalonia Demo案例Avalonia 开始使用Avalonia 文件扩展名Avalonia Toolkit 扩展安装修改.net core版本Avalonia对WPF的修改类CSS选择器Style的定义简单代码 数据绑定直接绑定UserControl.DataContext和Desig…