Go语言核心36讲21
提到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语言核心36讲21的更多相关文章
- Go语言核心36讲(导读)--学习笔记
目录 开篇词 | 跟着学,你也能成为Go语言高手 导读 | 写给0基础入门的Go语言学习者 导读 | 学习专栏的正确姿势 开篇词 | 跟着学,你也能成为Go语言高手 Go 语言是由 Google 出品 ...
- Go语言核心36讲(Go语言进阶技术八)--学习笔记
14 | 接口类型的合理运用 前导内容:正确使用接口的基础知识 在 Go 语言的语境中,当我们在谈论"接口"的时候,一定指的是接口类型.因为接口类型与其他数据类型不同,它是没法被实 ...
- Go语言核心36讲(Go语言进阶技术十五)--学习笔记
21 | panic函数.recover函数以及defer语句 (上) 在本篇,我要给你展示 Go 语言的另外一种错误处理方式.不过,严格来说,它处理的不是错误,而是异常,并且是一种在我们意料之外的程 ...
- Go语言核心36讲(Go语言进阶技术十六)--学习笔记
22 | panic函数.recover函数以及defer语句(下) 我在前一篇文章提到过这样一个说法,panic 之中可以包含一个值,用于简要解释引发此 panic 的原因. 如果一个 panic ...
- Go语言核心36讲(Go语言实战与应用一)--学习笔记
23 | 测试的基本规则和流程 (上) 在接下来的日子里,我将带你去学习在 Go 语言编程进阶的道路上,必须掌握的附加知识,比如:Go 程序测试.程序监测,以及 Go 语言标准库中各种常用代码包的正确 ...
- Go语言核心36讲(Go语言实战与应用三)--学习笔记
25 | 更多的测试手法 在本篇文章,我会继续为你讲解更多更高级的测试方法.这会涉及testing包中更多的 API.go test命令支持的,更多标记更加复杂的测试结果,以及测试覆盖度分析等等. 前 ...
- Go语言核心36讲(Go语言实战与应用四)--学习笔记
26 | sync.Mutex与sync.RWMutex 从本篇文章开始,我们将一起探讨 Go 语言自带标准库中一些比较核心的代码包.这会涉及这些代码包的标准用法.使用禁忌.背后原理以及周边的知识. ...
- Go语言核心36讲(Go语言实战与应用十四)--学习笔记
36 | unicode与字符编码 在开始今天的内容之前,我先来做一个简单的总结. Go 语言经典知识总结 在数据类型方面有: 基于底层数组的切片: 用来传递数据的通道: 作为一等类型的函数: 可实现 ...
- Go语言核心36讲(Go语言实战与应用十五)--学习笔记
37 | strings包与字符串操作 Go 语言不但拥有可以独立代表 Unicode 字符的类型rune,而且还有可以对字符串值进行 Unicode 字符拆分的for语句. 除此之外,标准库中的un ...
- Go语言核心36讲(Go语言实战与应用十八)--学习笔记
40 | io包中的接口和工具 (上) 我们在前几篇文章中,主要讨论了strings.Builder.strings.Reader和bytes.Buffer这三个数据类型. 知识回顾 还记得吗?当时我 ...
随机推荐
- .md图片链接转存并替换路径,及相关报错解决方法
最初我想把Typora中.md文件中的web图片链接都下载保存到本地,并且替换.md文本中的路径 说干就干,因为在网上没有找到现成的程序所以自己写了这个程序 思路是循环查找文件夹中的文件,然后yiel ...
- File类、FileOutputStream
day01 File类 File类的每一个实例可以表示硬盘(文件系统)中的一个文件或目录(实际上表示的是一个抽象路径) 使用File可以做到: 1:访问其表示的文件或目录的属性信息,例如:名字,大小, ...
- 基于anaconda3的Pytorch环境搭建
安装anaconda3,版本选择新的就行 打开anaconda prompt创建虚拟环境conda create -n pytorch_gpu python=3.9,pytorch_gpu是环境名称, ...
- 如何为 SAST 工具设置误报基准?
许多 SAST 工具都无法避免误报的问题.这些工具经常报告一些实际不存在的漏洞,这种不准确性让安全团队耗费大量时间来对误报进行分类和处理,这时设置误报基准就显得十分必要. 通过设置误报基准,安全团队可 ...
- SDN实验环境安装配置
- Rust-函数
函数结构 函数要点 函数名和变量名使用蛇形命名法(snake case),例如 fn add_two() -> {} 函数的位置可以随便放,Rust 不关心我们在哪里定义了函数,只要有定义即可 ...
- 【微服务】- 配置中心 - Nacos
微服务 - 配置中心 - Nacos 一个有梦有戏的人 @怒放吧德德 分享学习心得,欢迎指正,大家一起学习成长! 今天的学习任务就是学习使用Nacos作为配置中心. 努力克制自己,拒绝摆烂! 什么是配 ...
- Vmware部署Linux无人值守安装Centos7系统
Linux - 无人值守安装服务 # 需求分析 - 使用光盘镜像来安装 Linux 系统的方式; 坦白讲, 该方法适用于只安装少量 Linux 系统的情况, 如果生产环境中有数百台服务器都需安装系统, ...
- 第六章:Django 综合篇 - 12:聚合内容 RSS/Atom
Django提供了一个高层次的聚合内容框架,让我们创建RSS/Atom变得简单,你需要做的只是编写一个简单的Python类. 一.范例 要创建一个feed,只需要编写一个Feed类,然后设置一条指向F ...
- MySQL之pt-query-digest分析慢查询日志的详情介绍
一.简介 pt-query-digest是用于分析mysql慢查询的一个工具,它可以分析binlog.General log.slowlog,也可以通过SHOWPROCESSLIST或者通过tcpdu ...