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

news2024/12/28 20:20:19

前言

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

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

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

相关文章

SpringBoot项目如何打包成docker镜像?

将Spring Boot项目打包成Docker镜像的过程可以分为以下几个步骤&#xff1a; 1. 创建Dockerfile 首先&#xff0c;你需要在Spring Boot项目的根目录下创建一个Dockerfile。这个文件包含了Docker镜像构建的所有指令。一个基本的Dockerfile可能看起来像这样&#xff1a; # 使用…

flink sql 实战实例 及延伸问题:聚合/数据倾斜/DAU/Hive流批一体 等

flink sql 实战实例 及延伸问题 Flink SQL 计算用户分布Flink SQL 计算 DAU多topic 数据更新mysql topic接入mysql引入 upsert-kafka-connector 以1.14.4版本为例 数据倾斜问题&#xff1a;让你使用用户心跳日志&#xff08;20s 上报一次&#xff09;计算同时在线用户、DAU 指标…

Github代码仓库SSH配置流程

作者&#xff1a; Herman Ye Auromix 测试环境&#xff1a; Ubuntu20.04 更新日期&#xff1a; 2024/02/21 注1&#xff1a; Auromix 是一个机器人爱好者开源组织。 注2&#xff1a; 由于笔者水平有限&#xff0c;以下内容可能存在事实性错误。 相关背景 在为Github代码仓库配…

秦岭天台山隧道群荣获交通运输部科技示范工程,恒星科通群载波应急广播与无线调度系统产品应用其中

2023年9月12日&#xff0c;全国交通运输科技示范工程现场推进会在河南省平顶山市召开&#xff0c;会上为全国已通过验收的10项科技示范工程进行了授牌&#xff0c;其中由陕西交控集团负责实施的“秦岭天台山超长隧道群安全绿色科技示范工程”名列其中。 该科技示范工程为陕西省…

2024牛客寒假算法基础集训营4(视频讲解题目)

2024牛客寒假算法基础集训营4&#xff08;视频讲解题目&#xff09; 视频链接ABCDEFG、H&#xff08;下面是hard版本的代码两个都可以过&#xff09; 视频链接 2024牛客寒假算法基础集训营4&#xff08;视频讲解题目&#xff09; A #include<bits/stdc.h> #define en…

为全志D1开发板移植LVGL日历控件和显示天气

利用TCP封装HTTP包请求天气信息 Linux还真是逐步熟悉中&#xff0c;现在才了解到Linux即没有原生的GUI&#xff0c;也没有应用层协议栈&#xff0c;所以要实现HTTP应用&#xff0c;必须利用TCP然后自己封装HTTP数据包。本篇即记录封装HTTP数据包&#xff0c;到心知天气请求天气…

亿道丨三防平板电脑厂家丨三防平板PDA丨三防工业平板:数字时代

在当今数字化时代&#xff0c;我们身边的世界变得越来越依赖于智能设备和无线连接。其中&#xff0c;三防平板PDA&#xff08;Personal Digital Assistant&#xff09;作为一种功能强大且耐用的数字工具&#xff0c;正在引领我们进入数字世界的全新征程。 三防平板PDA结合了平板…

在Win系统部署WampServer并实现公网访问本地服务【内网穿透】

目录 推荐 前言 1.WampServer下载安装 2.WampServer启动 3.安装cpolar内网穿透 3.1 注册账号 3.2 下载cpolar客户端 3.3 登录cpolar web ui管理界面 3.4 创建公网地址 4.固定公网地址访问 推荐 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0…

【Pytorch深度学习开发实践学习】B站刘二大人课程笔记整理lecture02 线性模型

课程网址 Pytorch深度学习实践 部分课件内容&#xff1a; 代码部分如下&#xff1a; import numpy as np import matplotlib.pyplot as pltx_data [1.0,2.0,3.0] y_data [2.0,4.0,6.0]def forward(x):return x * wdef loss(x,y):y_pred forward(x)return (y_pred - y) ** …

vue实现列表自动无缝滚动列表

大家好&#xff0c;今天给大家分享的知识是vue基于vue-seamless-scroll实现自动无缝滚动列表 一、实现自动滚动 最近在开发过程中遇到一个问题&#xff0c;就是需要实现自动滚动列表&#xff0c;效果图如下 就是这样一个列表在自动循环展示。在这里我是运用的 vue-seamless-sc…

Springboot AOP开发

Springboot AOP开发 一 AOP概述 AOP&#xff0c;即面向切面编程&#xff0c;简言之&#xff0c;面向方法编程。 针对方法&#xff0c;在方法的执行前或执行后使用&#xff0c;用于增强方法&#xff0c;或拓展。 二 AOP开发 1.引入 spring-boot-starter-aop 在SpringBoot项…

计算机视觉基础【OpenCV轻松入门】:获取图像的ROI

OpenCV的基础是处理图像&#xff0c;而图像的基础是矩阵。 因此&#xff0c;如何使用好矩阵是非常关键的。 下面我们通过一个具体的实例来展示如何通过Python和OpenCV对矩阵进行操作&#xff0c;从而更好地实现对图像的处理。 ROI&#xff08;Region of Interest&#xff09;是…

十三、集合进阶——单列集合 及 数据结构

单列集合 及 数据结构 13.1 集合体系结构13.1.2 单列集合1. Collection2.Collection 的遍历方式迭代器遍历增强for遍历Lambda表达式遍历 3.List集合List集合的特有方法List集合的遍历方式五种遍历方式对比 4.数据结构1).栈2).队列3&#xff09;数组4&#xff09;链表小结5&…

【程序员英语】【美语从头学】初级篇(入门)(笔记)Lesson 15 At the Department Store 在百货商店

《美语从头学初级入门篇》 注意&#xff1a;被 删除线 划掉的不一定不正确&#xff0c;只是不是标准答案。 文章目录 Lesson 15 At the Department Store 在百货商店会话A会话B笔记 Lesson 15 At the Department Store 在百货商店 会话A A: Can you help me, please? B: Sur…

世强硬创与SMT激光模板制造商光盛激光达成平台合作

近日&#xff0c;世强先进&#xff08;深圳&#xff09;科技股份有限公司&#xff08;下称“世强先进”&#xff09;与一家专注生产研发电子工业用工模具的企业——东莞光盛激光科技有限公司&#xff08;下称“光盛激光”&#xff09;达成平台合作。 据了解&#xff0c;光盛激光…

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

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

绝地求生:图纸的加量不加价是否预示着蓝洞经营模式的转变

成长型武器目前作为PUBG中除了究极异色皮肤外的最高等级武器&#xff08;传说级&#xff09;&#xff0c;也是PUBG核心利润来源&#xff0c;十分的珍贵。 一把成长型武器的保底价格为3000碎片&#xff0c;而每次通过G-coin抽取会赠送10个碎片&#xff0c;也就是需要抽取三百次&…

外汇交易解决方案丨实时选取外汇行情多价源最优价

在外汇交易中&#xff0c;存在多个价源。多个价源之间&#xff0c;同一时刻的报价可能存在差异。在多个价源之间&#xff0c;实时选取最优价格具有重要意义&#xff0c;它有助于投资者获得更有利的交易执行价&#xff0c;降低交易成本&#xff0c;优化流动性&#xff0c;分散交…

中科大计网学习记录笔记(十四):多路复用与解复用 | 无连接传输:UDP

前言&#xff1a; 学习视频&#xff1a;中科大郑烇、杨坚全套《计算机网络&#xff08;自顶向下方法 第7版&#xff0c;James F.Kurose&#xff0c;Keith W.Ross&#xff09;》课程 该视频是B站非常著名的计网学习视频&#xff0c;但相信很多朋友和我一样在听完前面的部分发现信…

YOLO-NAS浅析

YOLO-NAS&#xff08;You Only Look Once - Neural Architecture Search&#xff09;是一种基于YOLO&#xff08;You Only Look Once&#xff09;的目标检测算法&#xff0c;结合神经架构搜索&#xff08;NAS&#xff09;技术来优化模型性能。 YOLO是一种实时目标检测算法&…