极客时间已完结课程限时免费阅读

19 | 错误处理(上)

19 | 错误处理(上)-极客时间

19 | 错误处理(上)

讲述:黄洲君

时长09:30大小3.27M

提到 Go 语言中的错误处理,我们其实已经在前面接触过几次了。
比如,我们声明过error类型的变量err,也调用过errors包中的New函数。今天,我会用这篇文章为你梳理 Go 语言错误处理的相关知识,同时提出一些关键问题并与你一起探讨。
我们说过error类型其实是一个接口类型,也是一个 Go 语言的内建类型。在这个接口类型的声明中只包含了一个方法ErrorError方法不接受任何参数,但是会返回一个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函数之后,都会把它返回的结果值赋给变量resperr,并且总是先检查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.PathErroros.LinkErroros.SyscallErroros/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子句,分别对应了上述几个错误类型。当它们被选中时,都会把函数参数errErr字段作为结果值返回。如果它们都未被选中,那么该函数就会直接把参数值作为结果返回,即放弃获取潜在错误值。
只要类型不同,我们就可以如此分辨。但是在错误值类型相同的情况下,这些手段就无能为力了。在 Go 语言的标准库中也有不少以相同方式创建的同类型的错误值。
我们还拿os包来说,其中不少的错误值都是通过调用errors.New函数来初始化的,比如:os.ErrClosedos.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.IsExistos.IsNotExistos.IsPermission。命令源码文件 demo45.go 中包含了对它们的应用,这大致跟前面展示的代码差不太多,我就不在这里赘述了。

总结

今天我们一起初步学习了错误处理的内容。我们总结了错误类型、错误值的处理技巧和设计方式,并一起分享了 Go 语言中处理错误的最基本方式。由于错误处理的内容分为上下两篇,在下一次的文章中,我们会站在建造者的角度,一起来探索一下:怎样根据实际情况给予恰当的错误值。

思考题

请列举出你经常用到或者看到的 3 个错误类型,它们所在的错误类型体系都是怎样的?你能画出一棵树来描述它们吗?
感谢你的收听,我们下期再见。
分享给需要的人,Ta购买本课程,你将得18
生成海报并分享

赞 15

提建议

上一篇
18 | if语句、for语句和switch语句
下一篇
20 | 错误处理 (下)
unpreview
 写留言

精选留言(22)

  • zs阿帅
    2018-09-24
    go2设计草案里提到说对于这种error处理的多个卫术语句的简化,利用check,handler简化错误处理的代码量。
    共 1 条评论
    20
  • CcczzZ
    2020-01-09
    之前要模板化的方式生成错误信息,都会用 errors.New() + fmt.Sprintf() 的组合去生成错误信息, errors.New(fmt.Sprintf())。 看完文章之后发现还有 fmt.Errorf函数 可直接模版话方式生成错误信息,方便不少,开心😄之前的方式真是不太优雅
    12
  • Felix
    2020-04-07
    老师,我一直想知道关于io.EOF怎么理解,拿文件来说如果文件读取结束后,为什么是EOF结束,或者说到底什么是EOF.

    作者回复: EOF 就是 End Of File 的缩写。早在 Unix 操作系统诞生之初就有 EOF 这种形式了,它最早象征着文件中内容的结束。由于一些原因,它被很多操作系统和编程语言沿用了。现在的 EOF 的用途已经远不止表示文件的末尾了,它在很多地方都表示某种数据流的末尾。

    10
  • 2019-07-27
    打卡 已经看了快2/5了 因为之前学过 只是会用 没有这么深入了解过 这次 很多知识点都有新的认识了
    7
  • 那句诺言
    2018-10-30
    老师,“该值的静态类型当然是error,而动态类型则是一个在errors包中的,包级私有的类型*errorString”,静态类型和动态类型该怎么理解呢?

    作者回复: 我讲接口的时候讲了啊。

    共 2 条评论
    7
  • 胡小涵
    2021-04-27
    郝老师您好,抱歉最近刚看到18年关于您近况的文章,想问一下您最近怎么样了?身体还好嘛?祝老师身体健康!平安顺遂!

    作者回复: 现在算是稳定了,或者说基本达到了另外一种平衡状态。谢谢关心:)

    共 3 条评论
    6
  • benying
    2019-06-05
    评论的人很少了,打卡,20190605
    共 2 条评论
    5
  • 海盗船长
    2020-11-26
    go1.13中,errors又新增了3个方法 errors.Unwrap errors.Is errors.As
    5
  • 海盗船长
    2019-03-15
    老师你好,在代码中如何优雅地处理错误呀,有没有办法 不需要在每个方法中处理,集中到一个地方处理?

    作者回复: 基本上是这样,就拿Web系统来说,一般情况下会在系统的最上层(接收request的那一层)统一处理错误。

    5
  • Laughing
    2018-10-06
    常用到的net和json包中的错误类型有: 1. AddrError 2. SyntaxError 3. MarshalerError
    4
  • A 凡
    2018-10-24
    感觉第一遍看的时候,看的不够仔细,所以从头有看到这部分,发现细节性的东西还是有很多需要注意!
    4
  • MClink
    2021-03-20
    echo 函数 return 后面的东西呢?

    作者回复: 因为那个函数存在有名称的结果声明,所以在该函数开始执行的时候,作为其结果的变量(如 echo 函数中的 response 变量和 err 变量)就已经自动创建好了。因此,这种函数中的 return 就可以不加任何东西,这时返回的就是那几个结果变量(如 response 和 err)的值。

    共 2 条评论
    3
  • 罗峰
    2021-06-06
    两个err 比较,应该是比较 相应的动态类型以及动态值是不是 完全一样,感觉就是地址的比较把

    作者回复: 是的。不过修正下你说的话,是“两个 error 类型值的比较”,不是“两个 err 比较”。

    1
  • 修罗
    2020-03-01
    打卡
    1
  • Geek_as
    2022-04-19
    underlyingError函数我还是有点懵逼,多个switch全部都是执行err.Err,那不需要switch直接执行不也可以吗

    作者回复: 这里的underlyingError函数的意思是,参数err的类型只要是*os.PathError、*os.LinkError、*os.SyscallError或*exec.Error中的任意一个的话,就返回err内含的(也就是潜在的)那个错误,否则就把参数err原封不动地返回去。函数如其名,它就是要设法拿到原错误的潜在错误。 当然,你也可以把那些用于判断的类型写到一个case里。我这里是为了在文章中清晰展示,所以把它们分开写了。

  • Z宇锤锤
    2022-04-08
    boundsError越界异常
  • Geek_05d654
    2022-03-03
    type PathError struct { Op string Path string Err error } func (e *PathError) Error() string { return e.Op + " " + e.Path + ": " + e.Err.Error() }
    展开
  • jxs1211
    2021-12-01
    判断2个接口类型(比如这里error类型的值)的值是否相等,是否就是判断他们的的实际类型和实际值是否相等,验证一下认为不是这样比较的,不是的话,什么情况下是相等的呢

    作者回复: 类型转换之后再比较啊,你可以用类型断言或者 type switch 语句去判断类型。但一般都会有 other(或者说 default)的情况,这需要妥善处理。

    共 3 条评论
  • 任小龙
    2021-05-27
    pkg/errors,学习中。
  • LH
    2019-03-31
    未知类型的错误真的感觉不太友好

    作者回复: 其实只是不太随大流而已。try...catch...finally的方式代码量也少不到哪里去。不过Go 2会有大幅改进的。