20 | 错误处理 (下)

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

知识扩展

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

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

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

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

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.AddrError或net.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包外的代码可以拿到这些代表错误的常量,但却无法改变它们。

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

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

package main

import (
"fmt"
"os"
"os/exec"
"strconv"
) // Errno 代表某种错误的类型。
type Errno int func (e Errno) Error() string {
return "errno " + strconv.Itoa(int(e))
} func main() {
var err error
// 示例1。
_, err = exec.LookPath(os.DevNull)
fmt.Printf("error: %s\n", err)
if execErr, ok := err.(*exec.Error); ok {
execErr.Name = os.TempDir()
execErr.Err = os.ErrNotExist
}
fmt.Printf("error: %s\n", err)
fmt.Println() // 示例2。
err = os.ErrPermission
if os.IsPermission(err) {
fmt.Printf("error(permission): %s\n", err)
} else {
fmt.Printf("error(other): %s\n", err)
}
os.ErrPermission = os.ErrExist
// 上面这行代码修改了os包中已定义的错误值。
// 这样做会导致下面判断的结果不正确。
// 并且,这会影响到当前Go程序中所有的此类判断。
// 所以,一定要避免这样做!
if os.IsPermission(err) {
fmt.Printf("error(permission): %s\n", err)
} else {
fmt.Printf("error(other): %s\n", err)
}
fmt.Println() // 示例3。
const (
ERR0 = Errno(0)
ERR1 = Errno(1)
ERR2 = Errno(2)
)
var myErr error = Errno(0)
switch myErr {
case ERR0:
fmt.Println("ERR0")
case ERR1:
fmt.Println("ERR1")
case ERR2:
fmt.Println("ERR2")
}
}

总结

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

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

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

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

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

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

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

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

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

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

思考题

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

笔记源码

https://github.com/MingsonZheng/go-core-demo

本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。

欢迎转载、使用、重新发布,但务必保留文章署名 郑子铭 (包含链接: http://www.cnblogs.com/MingsonZheng/ ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。

Go语言核心36讲(Go语言进阶技术十四)--学习笔记的更多相关文章

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

    16 | go语句及其执行规则(上) 我们已经知道,通道(也就是 channel)类型的值,可以被用来以通讯的方式共享数据.更具体地说,它一般被用来在不同的 goroutine 之间传递数据.那么 g ...

  2. Go语言核心36讲(Go语言基础知识三)--学习笔记

    03 | 库源码文件 在我的定义中,库源码文件是不能被直接运行的源码文件,它仅用于存放程序实体,这些程序实体可以被其他代码使用(只要遵从 Go 语言规范的话). 这里的"其他代码" ...

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

    24 | 测试的基本规则和流程(下) Go 语言是一门很重视程序测试的编程语言,所以在上一篇中,我与你再三强调了程序测试的重要性,同时,也介绍了关于go test命令的基本规则和主要流程的内容.今天我 ...

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

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

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

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

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

    07 | 数组和切片 我们这次主要讨论 Go 语言的数组(array)类型和切片(slice)类型. 它们的共同点是都属于集合类的类型,并且,它们的值也都可以用来存储某一种类型的值(或者说元素). 不 ...

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

    09 | 字典的操作和约束 至今为止,我们讲过的集合类的高级数据类型都属于针对单一元素的容器. 它们或用连续存储,或用互存指针的方式收纳元素,这里的每个元素都代表了一个从属某一类型的独立值. 我们今天 ...

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

    10 | 通道的基本操作 作为 Go 语言最有特色的数据类型,通道(channel)完全可以与 goroutine(也可称为 go 程)并驾齐驱,共同代表 Go 语言独有的并发编程模式和编程哲学. D ...

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

    11 | 通道的高级玩法 我们已经讨论过了通道的基本操作以及背后的规则.今天,我再来讲讲通道的高级玩法. 首先来说说单向通道.我们在说"通道"的时候指的都是双向通道,即:既可以发也 ...

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

    12 | 使用函数的正确姿势 在前几期文章中,我们分了几次,把 Go 语言自身提供的,所有集合类的数据类型都讲了一遍,额外还讲了标准库的container包中的几个类型. 在几乎所有主流的编程语言中, ...

随机推荐

  1. Java基础系列(1)- JDK、JRE、JVM

    Java三大版本(Write Once:Run Anywhere) JavaSE:标准版 JavaME:嵌入式开发 JavaEE:E企业级开发 JDK.JRE.JVM JDK是开发工具包 JRE是编译 ...

  2. P1909 [NOIP2016 普及组] 买铅笔

    如果她选择购买第一种包装,那么她需要购买29份,共计2×29=58支,需要花费的钱为2×29=58. 实际上,P老师会选择购买第三种包装,这样需要买22份.虽然最后买到的铅笔数 量更多了,为30×2= ...

  3. Ubuntu怎么开启/关闭防火墙

    在Ubuntu中,使用sudo ufw status命令查看当前防火墙状态. 不活动,是关闭状态. 在Ubuntu中,使用sudo ufw enable命令来开启防火墙. 在Ubuntu中,使用sud ...

  4. 在Windows Server 2012R2离线安装.net framework3.5

    最近新装了一台Windows Server 2012 R2的服务器,安装数据库时,出现了提示安装不上 .net framework3.5的情况,经过网络上多次的资料查找及反复试验终于找到了一个可以解决 ...

  5. 将Oracle数据库数据每天备份恢复一次数据到另一台服务器上两份数据

    1.创建用户,授权,创建测试数据 创建用户 CREATE USER test identified by 123; 授权 grant dba to test; 创建测试数据 create table ...

  6. 洛谷4895 独钓寒江雪 (树哈希+dp+组合)

    qwq 首先,如果是没有要求本质不同的话,那么还是比较简单的一个树形dp 我们令\(dp[i][0/1]\)表示是否\(i\)的子树,是否选\(i\)这个点的方案数. 一个比较显然的想法. \(dp[ ...

  7. 洛谷2151[SDOI2009]HH去散步(dp+矩阵乘法优化)

    一道良好的矩阵乘法优化\(dp\)的题. 首先,一个比较\(naive\)的想法. 我们定义\(dp[i][j]\)表示已经走了\(i\)步,当前在点\(j\)的方案数. 由于题目中限制了不能立即走之 ...

  8. 洛谷4299首都(LCT维护动态重心+子树信息)

    这个题目很有意思 QWQ 根据题目描述,我们可以知道,首都就是所谓的树的重心,那么我们假设每颗树的重心都是\(root\)的话,对于每次询问,我们只需要\(findroot(x)\)就可以. 那么如何 ...

  9. C#开发BIMFACE系列47 IIS部署并加载离线数据包

    BIMFACE二次开发系列目录     [已更新最新开发文章,点击查看详细] 在前两篇博客<C#开发BIMFACE系列45 服务端API之创建离线数据包>与<C#开发BIMFACE系 ...

  10. 掌握BeanShell,轻松处理jmeter中的数据

    作者:季沐测试笔记 原文地址:https://www.cnblogs.com/testero/p/15424558.html 博客主页:https://www.cnblogs.com/testero ...