你好,我是郝林,今天我们继续来分享错误处理。

在上一篇文章中,我们主要讨论的是从使用者的角度看“怎样处理好错误值”。那么,接下来我们需要关注的,就是站在建造者的角度,去关心“怎样才能给予使用者恰当的错误值”的问题了。

知识扩展

问题:怎样根据实际情况给予恰当的错误值?

我们已经知道,构建错误值体系的基本方式有两种,即:创建立体的错误类型体系和创建扁平的错误值列表。

先说错误类型体系。由于在Go语言中实现接口是非侵入式的,所以我们可以做得很灵活。比如,在标准库的net代码包中,有一个名为Error的接口类型。它算是内建接口类型error的一个扩展接口,因为errornet.Error的嵌入接口。

net.Error接口除了拥有error接口的Error方法之外,还有两个自己声明的方法:TimeoutTemporary

net包中有很多错误类型都实现了net.Error接口,比如:

  1. *net.OpError
  2. *net.AddrError
  3. net.UnknownNetworkError等等。

你可以把这些错误类型想象成一棵树,内建接口error就是树的根,而net.Error接口就是一个在根上延伸的第一级非叶子节点。

同时,你也可以把这看做是一种多层分类的手段。当net包的使用者拿到一个错误值的时候,可以先判断它是否是net.Error类型的,也就是说该值是否代表了一个网络相关的错误。

如果是,那么我们还可以再进一步判断它的类型是哪一个更具体的错误类型,这样就能知道这个网络相关的错误具体是由于操作不当引起的,还是因为网络地址问题引起的,又或是由于网络协议不正确引起的。

当我们细看net包中的这些具体错误类型的实现时,还会发现,与os包中的一些错误类型类似,它们也都有一个名为Err、类型为error接口类型的字段,代表的也是当前错误的潜在错误。

所以说,这些错误类型的值之间还可以有另外一种关系,即:链式关系。比如说,使用者调用net.DialTCP之类的函数时,net包中的代码可能会返回给他一个*net.OpError类型的错误值,以表示由于他的操作不当造成了一个错误。

同时,这些代码还可能会把一个*net.AddrErrornet.UnknownNetworkError类型的值赋给该错误值的Err字段,以表明导致这个错误的潜在原因。如果,此处的潜在错误值的Err字段也有非nil的值,那么将会指明更深层次的错误原因。如此一级又一级就像链条一样最终会指向问题的根源。

把以上这些内容总结成一句话就是,用类型建立起树形结构的错误体系,用统一字段建立起可追根溯源的链式错误关联。这是Go语言标准库给予我们的优秀范本,非常有借鉴意义。

不过要注意,如果你不想让包外代码改动你返回的错误值的话,一定要小写其中字段的名称首字母。你可以通过暴露某些方法让包外代码有进一步获取错误信息的权限,比如编写一个可以返回包级私有的err字段值的公开方法Err

相比于立体的错误类型体系,扁平的错误值列表就要简单得多了。当我们只是想预先创建一些代表已知错误的错误值时候,用这种扁平化的方式就很恰当了。

不过,由于error是接口类型,所以通过errors.New函数生成的错误值只能被赋给变量,而不能赋给常量,又由于这些代表错误的变量需要给包外代码使用,所以其访问权限只能是公开的。

这就带来了一个问题,如果有恶意代码改变了这些公开变量的值,那么程序的功能就必然会受到影响。因为在这种情况下我们往往会通过判等操作来判断拿到的错误值具体是哪一个错误,如果这些公开变量的值被改变了,那么相应的判等操作的结果也会随之改变。

这里有两个解决方案。第一个方案是,先私有化此类变量,也就是说,让它们的名称首字母变成小写,然后编写公开的用于获取错误值以及用于判等错误值的函数。

比如,对于错误值os.ErrClosed,先改写它的名称,让其变成os.errClosed,然后再编写ErrClosed函数和IsErrClosed函数。

当然了,这不是说让你去改动标准库中已有的代码,这样做的危害会很大,甚至是致命的。我只能说,对于你可控的代码,最好还是要尽量收紧访问权限。

再来说第二个方案,此方案存在于syscall包中。该包中有一个类型叫做Errno,该类型代表了系统调用时可能发生的底层错误。这个错误类型是error接口的实现类型,同时也是对内建类型uintptr的再定义类型。

由于uintptr可以作为常量的类型,所以syscall.Errno自然也可以。syscall包中声明有大量的Errno类型的常量,每个常量都对应一种系统调用错误。syscall包外的代码可以拿到这些代表错误的常量,但却无法改变它们。

我们可以仿照这种声明方式来构建我们自己的错误值列表,这样就可以保证错误值的只读特性了。

好了,总之,扁平的错误值列表虽然相对简单,但是你一定要知道其中的隐患以及有效的解决方案是什么。

总结

今天,我从两个视角为你总结了错误类型、错误值的处理技巧和设计方式。我们先一起看了一下Go语言中处理错误的最基本方式,这涉及了函数结果列表设计、errors.New函数、卫述语句以及使用打印函数输出错误值。

接下来,我提出的第一个问题是关于错误判断的。对于一个错误值来说,我们可以获取到它的类型、值以及它携带的错误信息。

如果我们可以确定其类型范围或者值的范围,那么就可以使用一些明确的手段获知具体的错误种类。否则,我们就只能通过匹配其携带的错误信息来大致区分它们的种类。

由于底层系统给予我们的错误信息还是很有规律可循的,所以用这种方式去判断效果还比较显著。但是第三方程序给出的错误信息很可能就没那么规整了,这种情况下靠错误信息去辨识种类就会比较困难。

有了以上阐释,当把视角从使用者换位到建造者,我们往往就会去自觉地仔细思考程序错误体系的设计了。我在这里提出了两个在Go语言标准库中使用很广泛的方案,即:立体的错误类型体系和扁平的错误值列表。

之所以说错误类型体系是立体的,是因为从整体上看它往往呈现出树形的结构。通过接口间的嵌套以及接口的实现,我们就可以构建出一棵错误类型树。

通过这棵树,使用者就可以一步步地确定错误值的种类了。另外,为了追根溯源的需要,我们还可以在错误类型中,统一安放一个可以代表潜在错误的字段。这叫做链式的错误关联,可以帮助使用者找到错误的根源。

相比之下,错误值列表就比较简单了。它其实就是若干个名称不同但类型相同的错误值集合。

不过需要注意的是,如果它们是公开的,那就应该尽量让它们成为常量而不是变量,或者编写私有的错误值以及公开的获取和判等函数,否则就很难避免恶意的篡改。

这其实是“最小化访问权限”这个程序设计原则的一个具体体现。无论怎样设计程序错误体系,我们都应该把这一点考虑在内。

思考题

请列举出你经常用到或者看到的3个错误值,它们分别在哪个错误值列表里?这些错误值列表分别包含的是哪个种类的错误?

戳此查看Go语言专栏文章配套详细代码。

Go语言核心36讲22的更多相关文章

  1. Go语言核心36讲(Go语言进阶技术十六)--学习笔记

    22 | panic函数.recover函数以及defer语句(下) 我在前一篇文章提到过这样一个说法,panic 之中可以包含一个值,用于简要解释引发此 panic 的原因. 如果一个 panic ...

  2. Go语言核心36讲(导读)--学习笔记

    目录 开篇词 | 跟着学,你也能成为Go语言高手 导读 | 写给0基础入门的Go语言学习者 导读 | 学习专栏的正确姿势 开篇词 | 跟着学,你也能成为Go语言高手 Go 语言是由 Google 出品 ...

  3. Go语言核心36讲(Go语言进阶技术八)--学习笔记

    14 | 接口类型的合理运用 前导内容:正确使用接口的基础知识 在 Go 语言的语境中,当我们在谈论"接口"的时候,一定指的是接口类型.因为接口类型与其他数据类型不同,它是没法被实 ...

  4. Go语言核心36讲(Go语言进阶技术十五)--学习笔记

    21 | panic函数.recover函数以及defer语句 (上) 在本篇,我要给你展示 Go 语言的另外一种错误处理方式.不过,严格来说,它处理的不是错误,而是异常,并且是一种在我们意料之外的程 ...

  5. Go语言核心36讲(Go语言实战与应用一)--学习笔记

    23 | 测试的基本规则和流程 (上) 在接下来的日子里,我将带你去学习在 Go 语言编程进阶的道路上,必须掌握的附加知识,比如:Go 程序测试.程序监测,以及 Go 语言标准库中各种常用代码包的正确 ...

  6. Go语言核心36讲(Go语言实战与应用三)--学习笔记

    25 | 更多的测试手法 在本篇文章,我会继续为你讲解更多更高级的测试方法.这会涉及testing包中更多的 API.go test命令支持的,更多标记更加复杂的测试结果,以及测试覆盖度分析等等. 前 ...

  7. Go语言核心36讲(Go语言实战与应用四)--学习笔记

    26 | sync.Mutex与sync.RWMutex 从本篇文章开始,我们将一起探讨 Go 语言自带标准库中一些比较核心的代码包.这会涉及这些代码包的标准用法.使用禁忌.背后原理以及周边的知识. ...

  8. Go语言核心36讲(Go语言实战与应用十四)--学习笔记

    36 | unicode与字符编码 在开始今天的内容之前,我先来做一个简单的总结. Go 语言经典知识总结 在数据类型方面有: 基于底层数组的切片: 用来传递数据的通道: 作为一等类型的函数: 可实现 ...

  9. Go语言核心36讲(Go语言实战与应用十八)--学习笔记

    40 | io包中的接口和工具 (上) 我们在前几篇文章中,主要讨论了strings.Builder.strings.Reader和bytes.Buffer这三个数据类型. 知识回顾 还记得吗?当时我 ...

  10. Go语言核心36讲(Go语言实战与应用二十二)--学习笔记

    44 | 使用os包中的API (上) 我们今天要讲的是os代码包中的 API.这个代码包可以让我们拥有操控计算机操作系统的能力. 前导内容:os 包中的 API 这个代码包提供的都是平台不相关的 A ...

随机推荐

  1. helm安装csi-driver-nfs-v4.1.0

    Application version v4.1.0 Chart version v4.1.0 获取chart包 helm repo add csi-driver-nfs https://raw.gi ...

  2. KingbaseES 数据库大小写敏感特性

    针对不同版本.是否启用大小写敏感,特征汇总如下:

  3. OpenHarmony3.0如何轻松连接华为云IoT设备接入平台?

    摘要:本文主要介绍基于OpenHarmony 3.0版本来对接华为云IoT设备接入IoTDA,以小熊派BearPi-HM_Nano开发板为例,使用huaweicloud_iot_link SDK对接华 ...

  4. 延宕执行,妙用无穷,Go lang1.18入门精炼教程,由白丁入鸿儒,Golang中defer关键字延迟调用机制使用EP17

    先行定义,延后执行.不得不佩服Go lang设计者天才的设计,事实上,defer关键字就相当于Python中的try{ ...}except{ ...}finally{...}结构设计中的finall ...

  5. day05-线程的应用04

    7.线程的应用03 7.4坦克大战5.0版 增加功能: 我方坦克在发射的子弹消亡之后,才能发射新的子弹==>拓展:发射多颗子弹怎么办,控制一次最多只能发射5颗子弹 让敌人坦克发射的子弹消亡之后, ...

  6. Java学习笔记:基本输入、输出数据操作实例分析

    Java学习笔记:基本输入.输出数据操作.分享给大家供大家参考,具体如下: 相关内容: 输出数据: print println printf 输入数据: Scanner 输出数据: JAVA中在屏幕中 ...

  7. Exchange如何将邮件转发给外部邮件地址

    Exchange如何将邮件转发给外部邮件地址 最近遇到一个需求.一位已经离职的员工需要将后续的邮件转发给他自己的私人邮箱.安全,行政的审核通过后,这个问题就到了技术部门了. Exchange可以很方便 ...

  8. G&GH05 删除文件和.gitignore

    注意事项与声明 平台: Windows 10 作者: JamesNULLiu 邮箱: jamesnulliu@outlook.com 博客: https://www.cnblogs.com/james ...

  9. LVGL 模拟仿真(Windows+CodeBlocks)

    一.准备材料 Code Blocks官网:https://www.codeblocks.org/ Code Blocks 汉化包:链接: https://pan.baidu.com/s/12zB5bD ...

  10. 基于electron+vue+element构建项目模板之【自定义标题栏&右键菜单项篇】

    1.概述 开发平台OS:windows 开发平台IDE:vs code 本篇章将介绍自定义标题栏和右键菜单项,基于electron现有版本安全性的建议,此次的改造中主进程和渲染进程彼此语境隔离,通过预 ...