CategoryResourceRepost/极客时间专栏/Go语言核心36讲/模块二:Go语言进阶技术/19 | 错误处理(上).md
louzefeng d3828a7aee mod
2024-07-11 05:50:32 +00:00

166 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<audio id="audio" title="19 | 错误处理(上)" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/87/ef/8721ac7cd9292553db302c15cb996bef.mp3"></audio>
提到Go语言中的错误处理我们其实已经在前面接触过几次了。
比如,我们声明过`error`类型的变量`err`,也调用过`errors`包中的`New`函数。今天我会用这篇文章为你梳理Go语言错误处理的相关知识同时提出一些关键问题并与你一起探讨。
我们说过`error`类型其实是一个接口类型也是一个Go语言的内建类型。在这个接口类型的声明中只包含了一个方法`Error`。`Error`方法不接受任何参数,但是会返回一个`string`类型的结果。它的作用是返回错误信息的字符串表示形式。
我们使用`error`类型的方式通常是,在函数声明的结果列表的最后,声明一个该类型的结果,同时在调用这个函数之后,先判断它返回的最后一个结果值是否“不为`nil`”。
如果这个值“不为`nil`”那么就进入错误处理流程否则就继续进行正常的流程。下面是一个例子代码在demo44.go文件中。
```
package main
import (
&quot;errors&quot;
&quot;fmt&quot;
)
func echo(request string) (response string, err error) {
if request == &quot;&quot; {
err = errors.New(&quot;empty request&quot;)
return
}
response = fmt.Sprintf(&quot;echo: %s&quot;, request)
return
}
func main() {
for _, req := range []string{&quot;&quot;, &quot;hello!&quot;} {
fmt.Printf(&quot;request: %s\n&quot;, req)
resp, err := echo(req)
if err != nil {
fmt.Printf(&quot;error: %s\n&quot;, err)
continue
}
fmt.Printf(&quot;response: %s\n&quot;, resp)
}
}
```
我们先看`echo`函数的声明。`echo`函数接受一个`string`类型的参数`request`,并会返回两个结果。
这两个结果都是有名称的,第一个结果`response`也是`string`类型的,它代表了这个函数正常执行后的结果值。
第二个结果`err`就是`error`类型的,它代表了函数执行出错时的结果值,同时也包含了具体的错误信息。
当`echo`函数被调用时,它会先检查参数`request`的值。如果该值为空字符串,那么它就会通过调用`errors.New`函数,为结果`err`赋值,然后忽略掉后边的操作并直接返回。
此时,结果`response`的值也会是一个空字符串。如果`request`的值并不是空字符串,那么它就为结果`response`赋一个适当的值,然后返回,此时结果`err`的值会是`nil`。
再来看`main`函数中的代码。我在每次调用`echo`函数之后,都会把它返回的结果值赋给变量`resp`和`err`,并且总是先检查`err`的值是否“不为`nil`”,如果是,就打印错误信息,否则就打印常规的响应信息。
这里值得注意的地方有两个。第一,在`echo`函数和`main`函数中,我都使用到了卫述语句。我在前面讲函数用法的时候也提到过卫述语句。简单地讲,它就是被用来检查后续操作的前置条件并进行相应处理的语句。
对于`echo`函数来说,它进行常规操作的前提是:传入的参数值一定要符合要求。而对于调用`echo`函数的程序来说,进行后续操作的前提就是`echo`函数的执行不能出错。
>
我们在进行错误处理的时候经常会用到卫述语句,以至于有些人会吐槽说:“我的程序满屏都是卫述语句,简直是太难看了!”
不过,我倒认为这有可能是程序设计上的问题。每个编程语言的理念和风格几乎都会有明显的不同,我们常常需要顺应它们的纹理去做设计,而不是用其他语言的编程思想来编写当下语言的程序。
再来说第二个值得注意的地方。我在生成`error`类型值的时候,用到了`errors.New`函数。
这是一种最基本的生成错误值的方式。我们调用它的时候传入一个由字符串代表的错误信息,它会给返回给我们一个包含了这个错误信息的`error`类型值。该值的静态类型当然是`error`,而动态类型则是一个在`errors`包中的,包级私有的类型`*errorString`。
显然,`errorString`类型拥有的一个指针方法实现了`error`接口中的`Error`方法。这个方法在被调用后,会原封不动地返回我们之前传入的错误信息。实际上,`error`类型值的`Error`方法就相当于其他类型值的`String`方法。
我们已经知道,通过调用`fmt.Printf`函数,并给定占位符`%s`就可以打印出某个值的字符串表示形式。
对于其他类型的值来说,只要我们能为这个类型编写一个`String`方法,就可以自定义它的字符串表示形式。而对于`error`类型值,它的字符串表示形式则取决于它的`Error`方法。
在上述情况下,`fmt.Printf`函数如果发现被打印的值是一个`error`类型的值,那么就会去调用它的`Error`方法。`fmt`包中的这类打印函数其实都是这么做的。
顺便提一句,当我们想通过模板化的方式生成错误信息,并得到错误值时,可以使用`fmt.Errorf`函数。该函数所做的其实就是先调用`fmt.Sprintf`函数,得到确切的错误信息;再调用`errors.New`函数,得到包含该错误信息的`error`类型值,最后返回该值。
好了,我现在问一个关于对错误值做判断的问题。我们今天的**问题是对于具体错误的判断Go语言中都有哪些惯用法**
由于`error`是一个接口类型,所以即使同为`error`类型的错误值,它们的实际类型也可能不同。这个问题还可以换一种问法,即:怎样判断一个错误值具体代表的是哪一类错误?
这道题的**典型回答**是这样的:
1. 对于类型在已知范围内的一系列错误值,一般使用类型断言表达式或类型`switch`语句来判断;
1. 对于已有相应变量且类型相同的一系列错误值,一般直接使用判等操作来判断;
1. 对于没有相应变量且类型未知的一系列错误值,只能使用其错误信息的字符串表示形式来做判断。
## 问题解析
如果你看过一些Go语言标准库的源代码那么对这几种情况应该都不陌生。我下面分别对它们做个说明。
类型在已知范围内的错误值其实是最容易分辨的。就拿`os`包中的几个代表错误的类型`os.PathError`、`os.LinkError`、`os.SyscallError`和`os/exec.Error`来说,它们的指针类型都是`error`接口的实现类型,同时它们也都包含了一个名叫`Err`,类型为`error`接口类型的代表潜在错误的字段。
如果我们得到一个`error`类型值,并且知道该值的实际类型肯定是它们中的某一个,那么就可以用类型`switch`语句去做判断。例如:
```
func underlyingError(err error) error {
switch err := err.(type) {
case *os.PathError:
return err.Err
case *os.LinkError:
return err.Err
case *os.SyscallError:
return err.Err
case *exec.Error:
return err.Err
}
return err
}
```
函数`underlyingError`的作用是:获取和返回已知的操作系统相关错误的潜在错误值。其中的类型`switch`语句中有若干个`case`子句,分别对应了上述几个错误类型。当它们被选中时,都会把函数参数`err`的`Err`字段作为结果值返回。如果它们都未被选中,那么该函数就会直接把参数值作为结果返回,即放弃获取潜在错误值。
只要类型不同我们就可以如此分辨。但是在错误值类型相同的情况下这些手段就无能为力了。在Go语言的标准库中也有不少以相同方式创建的同类型的错误值。
我们还拿`os`包来说,其中不少的错误值都是通过调用`errors.New`函数来初始化的,比如:`os.ErrClosed`、`os.ErrInvalid`以及`os.ErrPermission`,等等。
注意,与前面讲到的那些错误类型不同,这几个都是已经定义好的、确切的错误值。`os`包中的代码有时候会把它们当做潜在错误值,封装进前面那些错误类型的值中。
如果我们在操作文件系统的时候得到了一个错误值,并且知道该值的潜在错误值肯定是上述值中的某一个,那么就可以用普通的`switch`语句去做判断,当然了,用`if`语句和判等操作符也是可以的。例如:
```
printError := func(i int, err error) {
if err == nil {
fmt.Println(&quot;nil error&quot;)
return
}
err = underlyingError(err)
switch err {
case os.ErrClosed:
fmt.Printf(&quot;error(closed)[%d]: %s\n&quot;, i, err)
case os.ErrInvalid:
fmt.Printf(&quot;error(invalid)[%d]: %s\n&quot;, i, err)
case os.ErrPermission:
fmt.Printf(&quot;error(permission)[%d]: %s\n&quot;, i, err)
}
}
```
这个由`printError`变量代表的函数会接受一个`error`类型的参数值。该值总会代表某个文件操作相关的错误,这是我故意地以不正确的方式操作文件后得到的。
虽然我不知道这些错误值的类型的范围,但却知道它们或它们的潜在错误值一定是某个已经在`os`包中定义的值。
所以,我先用`underlyingError`函数得到它们的潜在错误值,当然也可能只得到原错误值而已。然后,我用`switch`语句对错误值进行判等操作,三个`case`子句分别对应我刚刚提到的那三个已存在于`os`包中的错误值。如此一来,我就能分辨出具体错误了。
对于上面这两种情况,我们都有明确的方式去解决。但是,如果我们对一个错误值可能代表的含义知之甚少,那么就只能通过它拥有的错误信息去做判断了。
好在我们总是能通过错误值的`Error`方法,拿到它的错误信息。其实`os`包中就有做这种判断的函数,比如:`os.IsExist`、`os.IsNotExist`和`os.IsPermission`。命令源码文件demo45.go中包含了对它们的应用这大致跟前面展示的代码差不太多我就不在这里赘述了。
## 总结
今天我们一起初步学习了错误处理的内容。我们总结了错误类型、错误值的处理技巧和设计方式并一起分享了Go语言中处理错误的最基本方式。由于错误处理的内容分为上下两篇在下一次的文章中我们会站在建造者的角度一起来探索一下怎样根据实际情况给予恰当的错误值。
## 思考题
请列举出你经常用到或者看到的3个错误类型它们所在的错误类型体系都是怎样的你能画出一棵树来描述它们吗
感谢你的收听,我们下期再见。
[戳此查看Go语言专栏文章配套详细代码。](https://github.com/hyper0x/Golang_Puzzlers)