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. TP5缩放图片加水印

    // 给图片增加水印文字 试验缩放图片,放大图片,加水印,加文字功能 public function doCreateImage1($data,$path) { $basePath = ROOT_PA ...

  2. linux 服务器资源 监控工具

    工具一:vmstat(服务端) 一.vmstat选项参数解释 -V:显示vmstat版本信息 -n:只在开始时显示一次各字段名称 -a:显示活跃和非活跃内存 -d:显示各个磁盘相关统计信息 -D:显示 ...

  3. php 页面公共部分 转化为js document.write(); 并由匿名函数包裹

    页面公共部分以javascript  document.write()方式加载 生成的js放到需要的位置   footer.js 放到body底部引入 ... <script src=" ...

  4. css布局宽度自适应

    随着各种终端的不断涌现,网页中的元素适应不同的分辨率变得特别重要,根据经验,涉及到宽度自适应的一共有四种情况: 左端固定,右边自适应:右端固定,左边自适应:两端固定,中间自适应:中间固定,两端自适应. ...

  5. Dockerfile 的常用参数注解和范例

    一. docker hello world 1.1 Dockerfile FROM centos:7.5.1804 MAINTAINER 11@qq.com CMD echo "hello ...

  6. 我惊了!CompletableFuture居然有性能问题!

    你好呀,我是歪歪. 国庆的时候闲来无事,就随手写了一点之前说的比赛的代码,目标就是保住前 100 混个大赛的文化衫就行了. 现在还混在前 50 的队伍里面,稳的一比. 其实我觉得大家做柔性负载均衡那题 ...

  7. 零基础怎么学Java?Java的运行机制是什么?Java入门基础!

    Java语言是当前流行的一种程序设计语言,因其安全性.平台无关性.性能优异等特点,受到广大编程爱好者的喜爱. 想学习Java语言的同学对于Java的运行机制是必须要了解的!! 计算机高级语言的类型主要 ...

  8. 「JOISC 2020 Day2」变态龙之色 题解

    题目传送门 注意 同性必定不同色 必有一个同色异性,且不相互不喜欢 Solution 我们发现,我们问题比较大的就是如何确定性别问题.我们可以一个一个加进去,在原来已经确定了的二分图上增加新的性别关系 ...

  9. C 库函数源码

    github URL git://sourceware.org/git/glibc.git 码云 URL https://gitee.com/jason.R.xie/glibc.git

  10. 【Java虚拟机8】自定义类加载器、类加载器命名空间、类的卸载

    前言 学习类加载器就一定要自己实现一个类加载器,今天就从一个简单的自定义类加载器说起. 自定义类加载器 例1 一个简单的类加载器,从一个给定的二进制名字读取一个字节码文件的内容,然后生成对应的clas ...