learn-tech/专栏/Go语言核心36讲/19错误处理(上).md
2024-10-16 00:01:16 +08:00

168 lines
10 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.

因收到Google相关通知网站将会择期关闭。相关通知内容
19 错误处理(上)
提到Go语言中的错误处理我们其实已经在前面接触过几次了。
比如我们声明过error类型的变量err也调用过errors包中的New函数。今天我会用这篇文章为你梳理Go语言错误处理的相关知识同时提出一些关键问题并与你一起探讨。
我们说过error类型其实是一个接口类型也是一个Go语言的内建类型。在这个接口类型的声明中只包含了一个方法Error。Error方法不接受任何参数但是会返回一个string类型的结果。它的作用是返回错误信息的字符串表示形式。
我们使用error类型的方式通常是在函数声明的结果列表的最后声明一个该类型的结果同时在调用这个函数之后先判断它返回的最后一个结果值是否“不为nil”。
如果这个值“不为nil”那么就进入错误处理流程否则就继续进行正常的流程。下面是一个例子代码在demo44.go文件中。
package main
import (
"errors"
"fmt"
)
func echo(request string) (response string, err error) {
if request == "" {
err = errors.New("empty request")
return
}
response = fmt.Sprintf("echo: %s", request)
return
}
func main() {
for _, req := range []string{"", "hello!"} {
fmt.Printf("request: %s\n", req)
resp, err := echo(req)
if err != nil {
fmt.Printf("error: %s\n", err)
continue
}
fmt.Printf("response: %s\n", 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类型的错误值它们的实际类型也可能不同。这个问题还可以换一种问法怎样判断一个错误值具体代表的是哪一类错误
这道题的典型回答是这样的:
对于类型在已知范围内的一系列错误值一般使用类型断言表达式或类型switch语句来判断
对于已有相应变量且类型相同的一系列错误值,一般直接使用判等操作来判断;
对于没有相应变量且类型未知的一系列错误值,只能使用其错误信息的字符串表示形式来做判断。
问题解析
如果你看过一些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("nil error")
return
}
err = underlyingError(err)
switch err {
case os.ErrClosed:
fmt.Printf("error(closed)[%d]: %s\n", i, err)
case os.ErrInvalid:
fmt.Printf("error(invalid)[%d]: %s\n", i, err)
case os.ErrPermission:
fmt.Printf("error(permission)[%d]: %s\n", i, err)
}
}
这个由printError变量代表的函数会接受一个error类型的参数值。该值总会代表某个文件操作相关的错误这是我故意地以不正确的方式操作文件后得到的。
虽然我不知道这些错误值的类型的范围但却知道它们或它们的潜在错误值一定是某个已经在os包中定义的值。
所以我先用underlyingError函数得到它们的潜在错误值当然也可能只得到原错误值而已。然后我用switch语句对错误值进行判等操作三个case子句分别对应我刚刚提到的那三个已存在于os包中的错误值。如此一来我就能分辨出具体错误了。
对于上面这两种情况,我们都有明确的方式去解决。但是,如果我们对一个错误值可能代表的含义知之甚少,那么就只能通过它拥有的错误信息去做判断了。
好在我们总是能通过错误值的Error方法拿到它的错误信息。其实os包中就有做这种判断的函数比如os.IsExist、os.IsNotExist和os.IsPermission。命令源码文件demo45.go中包含了对它们的应用这大致跟前面展示的代码差不太多我就不在这里赘述了。
总结
今天我们一起初步学习了错误处理的内容。我们总结了错误类型、错误值的处理技巧和设计方式并一起分享了Go语言中处理错误的最基本方式。由于错误处理的内容分为上下两篇在下一次的文章中我们会站在建造者的角度一起来探索一下怎样根据实际情况给予恰当的错误值。
思考题
请列举出你经常用到或者看到的3个错误类型它们所在的错误类型体系都是怎样的你能画出一棵树来描述它们吗
感谢你的收听,我们下期再见。
戳此查看Go语言专栏文章配套详细代码。