通向Golang的捷径【13. 错误处理和测试】

Go 语言并无提供异常机制, 也就是 Java 或.NET 中出现的 try/catch 语句, 所以在 Go 语言中, 无法抛出异常, 而是使用了另一种机制, 延期-故障-恢复 (defer-panic-and-recover),Go 语言设计者认为 try/catch 机制有可能被滥用, 如果将底层异常抛向上层, 则会占用太多资源, 因此 Go 语言设计者使用另一种机制, 以使 Go 语言能捕获异常, 虽然这是一种轻量级机制, 也不能随意使用, 应将其视为最后的杀手锏.

对于一般错误,Go 语言的默认处理方式如下, 函数和方法可返回一个错误对象, 如果在执行中未出现错误, 错误对象为 nil, 因此忽略函数和方法返回的错误对象, 则可能引发应用程序的崩溃!

如果函数的执行中出现错误, 将在错误处理的过程中, 向用户发送一条错误信息, 所以程序即使存在一些问题,还是可以继续执行, 同时也提醒了用户, 但 panic-and-recover 机制通常用于异常 (无法预期的) 问题, 而不会用于一般错误的处理.

标准库程序必须返回一些错误码, 以标识函数的执行状态, 在之前的章节中, 已给出 Go 语言的常用编码方式,也就是对返回的错误码 (对象) 进行检查和报告:
• 函数可返回两个变量, 一是返回值, 其次是错误码, 如果函数执行成功, 错误码将为 nil, 否则错误码将为
非 nil 值.
• 当函数调用完成, 必须对错误码进行检查, 如果错误码不等于 nil, 应用程序的执行有可能被停止.

以下示例将检查 pack1 包函数 Func1 所返回的错误码:
在这里插入图片描述
可在一个 if 语句中, 实现错误码的赋值, 以使代码更加简明, 另外可使用 log 包的方法, 来替代 fmt.Printf(参见 13.3 节和 15.2 节), 即使出现一个故障 (panic), 也不会导致应用程序的终止.

13.1 错误处理

Go 语言中给出了一个预定义的 error 接口类型, 如下:
在这里插入图片描述
error 值可表示一种错误状态, 它的标准用法可参考 5.2 节, 其他示例可参考第 12 章的文件处理, 在第 15 章中, 给出了网络操作中 error 的用法, 在 errors 包中, 提供了一个 errorString 结构类型, 同时它实现了 error 接口, 在错误状态下, 为了终止应用程序的执行, 可使用 os.Exit(1).

13.1.1 错误定义

如果需要一种新的错误类型, 可使用 errors 包函数 errors.New, 并传入一个适合的字符串 (用于描述错误), 如下:
在这里插入图片描述

例 13.1 errors.go

在这里插入图片描述
在以下的开方函数中, 将对传入实参进行检查:
在这里插入图片描述
由于 fmt.Printf 可自动调用 String() 方法 (参见 10.7 节), 因此将输出错误信息 math - square root of negative number, 由于错误消息通常会包含一个Error: 前缀, 所以错误消息的首个字符不要使用大写字母.

在大多数情况下, 都需要创建一个自定义的错误类型 (结构), 以便在错误消息中添加一些有价值的信息, 比如当前执行的操作 (如打开文件等),url 或是完整路径名等信息, 而 String() 方法可将所有信息, 组合成一个字符串, 比如以下示例, 在 os.Open 操作中, 出现了 PathError(路径错误):
在这里插入图片描述
当出现不同的错误码时, 则需要使用一个类型断言或类型 switch 语句, 确定不同的错误码, 以处理或恢复对应的错误.
在这里插入图片描述
在这里插入图片描述
第二个代码段可在 json 包中使用, 当 JSON 文档解析中出现一个语法错误,json.Decode 函数可返回一个 SyntaxError 类型:
在这里插入图片描述
在 15.1 节中, 将会使用上述接口, 因此在所有示例中, 必须使用以下的名称转换规则, 错误类型需使用 Error后缀, 错误变量将设定为 err 或 Err.

syscall 是一个底层扩展包, 它可为操作系统的系统调用, 提供一个基本接口, 并且能返回整型错误码, syscall.Errno类型实现了 Error 接口. 大多数 syscall 函数都可返回执行结果值和错误码, 如下:
在这里插入图片描述
os 包提供了一组标准的错误变量集合, 比如 os.EINVAL, 它们已关联到 syscall 包的错误码.
在这里插入图片描述

13.1.2 创建错误对象

有时在返回的错误参数中, 需要给出一个包含错误信息的字符串, 因此需实现 fmt.Errorf() 函数, 而它与 fmt.Printf() 很相似, 可基于一个或多个格式化标志和对应个数的变量, 组合成一个格式化字符串, 只不过该字符串并不会打印, 而是作为一个包含信息的错误对象. 可将 fmt.Errorf() 函数, 用于之前的开方示例, 如下:
在这里插入图片描述
当命令行读取时, 出现一个错误, 则可生成一个具有帮助性质的错误对象,
在这里插入图片描述

13.2 运行时异常和故障

当程序执行出现错误时, 比如数组索引值越界, 或是类型断言失败,Go 运行时状态将触发一个运行时故障 (runtime panic), 并给出一个 runtime.Error 接口类型, 应用程序将会崩溃, 并发出一个错误消息, 该消息来自于RuntimeError() 方法, 以区别一般错误.

一个故障 (panic) 可在代码中直接初始化, 当出现的错误 (在测试过程中) 很严重且无法自动恢复 (应用程序无法继续执行), 将使用 panic 函数, 它可高效创建一个运行时错误, 以终止应用程序的执行, 该函数包含了一个任意类型的形参, 通常是一个字符串, 在应用程序终止时, 该字符串将被打印, 这时 Go 运行时状态将会停止应用程序, 并给出一些调试信息, 如下:

例 13.2 panic.go

在这里插入图片描述
在实际的应用程序中, 还可以检查启动应用程序的用户:
在这里插入图片描述
如果在嵌套函数 (函数的多阶调用) 中调用 panic, 它可立即终止当前函数的执行, 所有的 defer 语句能被执行,之后会将控制权交给当前函数的调用者, 而调用者也将执行 panic, 这类操作将一直上溯到顶层调用者, 它也将执行 defer 语句, 即堆栈处于顶层时 (无下层调用), 应用程序将崩溃, 并使用传递给 panic 的信息, 在命令行中打印出一个错误消息, 上述的操作流程被称为故障机制 (panicking).

标准库给出了一些以 Must 为前缀的函数, 比如 regexp.MustCompile 或 template.Must, 在这些函数的 panic()中, 可将字符串转换成一个正则表达式或模板, 以创建一个错误消息. 当然使用 panic 实现应用程序的逐级处理, 并不够廉价, 所以在每一级的处理中, 必须对错误进行足够的补救, 以使应用程序能够继续执行.

13.3 恢复

从名称上就能知道, 这是一个内建函数, 它可实现 panic 或错误状态下的恢复, 并在故障协程 (goroutine) 中,允许应用程序对错误进行补救, 以停止故障机制的上溯, 重新恢复应用程序的执行.

在 defer 函数中, 调用 recover 函数, 是一种极具价值的操作方式, 它对传递给 panic 调用的错误进行补救, 若应用程序处于正常执行状态,recover 将返回 nil, 且不会造成任何影响. 当延期 recover() 函数出现, 或是应用程序终止后,panic 调用可清空堆栈.

以下的 protect 函数, 将调用另一个函数 g, 从而对 g 上传的运行时故障进行保护, 其中给出 panic 的消息 x:
在这里插入图片描述
上述处理方法与 Java 和.NET 的 catch 块很相似.log 包实现了一个简单的日志功能, 默认的日志管理器可将标准错误和日期与时间 (发生错误时), 都写入每条日志消息, 除了 Println 和 Printf 函数, 还可在写入日志消息后, 在 Fatal 函数中调用 os.Exit(1), 也可直接使用 Exit 函数, 而 panic 可在写入日志消息后, 启动故障机制, 当临界条件出现时, 应用程序必须终止, 比如一个 web 服务器并未启动, 可参考 15.4 节的示例.

在 log 包中, 还定义了一个 Logger 接口类型, 并给出了一个同名方法, 当需要自定义一个日志系统时, 可使用该接口, 可参考页面http://golang.org/pkg/log/#Logger. 以下示例给出了 panic,defer 和 recover 的综合处理操作.

例 13.3 panic_recover.go

在这里插入图片描述
在这里插入图片描述
从上例可知,panic,defer 和 recover 也是一类控制流, 与 if,for 语句很相似, 在 Go 标准库中, 也使用了上述机制, 比如 json 包的解码操作,regexp 包的 Compile 函数中, 为了简化 Go 库的创建, 在包中会使用 panic, 而在recover 的执行中, 外部 API 将一直得到一个显式错误的返回.

13.4 自定义包的错误处理和故障处理

自定义包的每个写入器都可应用以下的最佳推荐规则:
• 在自定义包中, 故障一直需要配置一个 recover(恢复器), 任何显式(在代码中出现)panic() 都不允许实现包外的跳转.
• 返回 error, 等同于将错误信息返回给自定义包 (本包) 的调用者.

在自定义包中, 尤其是非导出函数需要给出深度的嵌套调用时, 使用 panic 可改善代码的可读性以及提供强大的功能, 它可定义一个错误信息, 并在函数调用之间进行传递.

以下示例给出了一个简单的 parse 包 (例 13.4), 它可将输入的字符串解析成一个整型 slice, 并包含了特殊的 ParseError. 在 fields2numbers 函数中, 当输入无法转换或是转换出错时, 将给出一个故障, 这时可导出的Parse 函数能实现上述故障的恢复, 并返回一个错误信息, 给 fields2numbers 函数的调用者, 而在示例中, 如果字符串无法被解析, 将打印出一个错误.

例 13.4 parse.go在这里插入图片描述

在这里插入图片描述

例 13.5 panic_package.go

在这里插入图片描述
在这里插入图片描述

13.5 通用的错误处理方案

当函数返回时, 都需要测试该函数的执行中, 是否出现了一个错误, 这将会导致重复的冗长的代码风格, 而组合使用defer/panic/recover 机制, 可实现一种优雅的错误处理方案, 但这个方案只能用于所有的同名函数中, 当然这类函数风格存在一些限制, 而 web 应用程序则是上述方案的一个示例, 同时所有的处理器函数都将使用以下原型:
在这里插入图片描述
在上述方案中, 需要另两个函数:
• check: 可检查错误是否出现, 并可生成一个故障:
在这里插入图片描述
• errorhandler: 这是一个封装函数, 它包含了一个 fType1 类型的函数 fn, 当 errorhandler 返回时, 将调用fn, 因此其中可包含 defer/recover 机制,
在这里插入图片描述
当错误进行恢复时, 将打印出一个日志, 除了简单信息的打印之外, 还可使用 template 包 (参见 15.7 节) 创建一个自定义的输出日志, 同时每个函数调用中, 都会使用 check() 函数, 如下:
在这里插入图片描述
在这里插入图片描述
main() 或其他函数调用者, 都可使用 errorHandler 封装所需的函数, 如下:
在这里插入图片描述
使用上述机制, 所有错误都能进行恢复, 而函数调用完成后, 进行的错误检查代码, 也可简化为 check(err), 同时在不同的错误处理中, 也可使用不同的函数类型, 而它们将被隐藏在一个错误处理包中, 另一种更通用的方案, 则是使用一个空接口的 slice, 作为函数的形参和返回类型 (之后可使用任意类型, 替代空接口), 在 15.5 节的 web 应用中, 将使用空接口方案.

13.6 运行外部命令或程序

在 os 包中, 将包含 StartProcess 函数, 它可启动 OS 平台下的系统命令或二进制程序, 它的第一个形参为需运行的程序名, 第二个形参为传递给启动程序的一些选项或参数, 第三个形参是一个结构变量, 它将提供 OS 环境的一些基本信息. 该函数可返回一个程序启动后的进程 ID 号 (pid), 如果启动失败, 将返回一个错误.

exec 包给出的结构和函数, 可实现与 os 包类似的功能, 而 exec.Command(name string, arg …string) 和 Run()更常用, 在上述函数中, 首先需提供 OS 命令名或可执行文件名, 并会创建一个 Command 对象, 之后将在Run() 的执行中, 使用该对象, 因此 Run 将其视为 Command 对象的一个接收器, 以下示例只能运行在 Linux系统中, 因为它将执行一个 Linux 系统命令:

例 13.6 exec.go

在这里插入图片描述
在这里插入图片描述

13.7 测试与评估

在每个包中, 首先会提供一个最简单的文档说明, 其次是一些相当重要的测试结果.

第 3 章提供了 Go 语言的测试工具 gotest, 同时在 9.8 节中使用了该工具, 在这里的一些示例中, 也将使用该工具. 目前存在一个特殊的 testing 包, 它可提供自动化测试, 日志报告和错误报告等功能, 同时还包含了一些评估功能.

gotest 是一个 Unix bash 脚本 (shell 脚本), 所以在 Windows 系统下, 必须给出一个 MINGW 环境 (参见 2.5节), 同时在 Windows 系统下, 需将 pkg/linux_amd64 替换成 pkg/windows.

为了对包进行测试 (单元测试), 需要编写一些测试用例, 以便经常运行 (即每次更新完毕后), 来检查代码的正确性, 因此必须编写一组 Go 源码文件, 对应用代码进行测试, 而测试程序必须放入所需测试的包中, 同时测试程序必须给出 _test 后缀, 以便在包中, 可区分出测试代码和应用代码.

而这些测试文件不会在正常编译中进行编译, 所以它们不会被放入应用程序中, 只有 gotest 可编译测试程序,在测试程序中, 必须导入 testing 包, 因此在编写全局函数时, 必须添加 Test 前缀, 比如 TestFmtInterface 或TestPayEmployees. 同时还可使用以下原型:
在这里插入图片描述
其中 T 为结构类型, 可传递给测试函数, 它可管理测试状态, 以及提供格式化的测试日志, 比如 t.Log, t.Error, t.ErrorF 等, 在每个测试函数的末尾, 将确认测试函数是否被导出, 以及在错误日志中, 是否给出了正确的测试函数名, 而运行成功的测试函数将会立即返回.

以下函数可发出一个失败信号:
• func (t *T) Fail()
表示测试函数中已出现测试失败, 但测试函数将会继续执行.

• func (t *T) FailNow()
表示测试函数中已出现测试失败, 而测试函数不会继续执行. 当前测试文件所剩余的其他测试将被跳过, 并从下一个测试文件开始, 继续进行测试.

• func (t *T) Log(args …interface)
args 需提供一个默认的格式化字符串, 登录信息将放入错误日志中.

• func (t *T) Fatal(args …interface)
合并了前两个函数的功能.

运行 gotest 可实现测试程序的编译, 并会执行测试程序包含的所有测试函数 (以 Test 为前缀), 如果所有测试都成功完成, 将会打印 PASS 字串. 在 gotest 的形参中, 可包含一个或多个测试程序, 以及一些执行选项, 如果使用–chatty 或–v 选项, 每个测试功能在运行时, 都将会输出消息, 同时也会输出它们的测试结果, 如下:
在这里插入图片描述
在 testing 包中, 还包含了评估功能所需的一些类型和函数, 同时测试代码必须包含在以 Benchmark 为前缀的函数中, 而这类函数可包含一个testing.B 类型的形参, 比如:
在这里插入图片描述
使用命令go test –test.bench=.
可执行所有的评估函数, 同时重复调用这些函数的次数会非常大 (比如 1000,000次), 基于重复调用的次数, 可计算出这些函数的平均执行时间 (ns), 在 14.16 节中, 使用了并发协程, 来实现评估功能.

13.8 测试示例

even 包提供了奇偶的检查功能, 如下:
在这里插入图片描述
even 包的实现代码如下:
在这里插入图片描述
在这里插入图片描述
在 even 包中, 可创建一个测试程序:
在这里插入图片描述
必须基于实际的输入条件, 才可完成对应的测试, 因此无法实现所有条件下的测试 (有限的测试条件下), 所以必须给出一些特殊测试条件, 至少应当包含以下条件:
• 一个正常的测试条件
• 不规则的测试条件, 比如负数, 以字母替代数字的无效输入, 以及无输入等条件
• 边界测试条件, 如果参数是一个 0-1000 之间的数值, 则需要检查 0 和 1000 的输入条件

除了直接使用命令 go install even(包安装), 还可创建一个 Makefile 文件, 完成对应的包安装:
在这里插入图片描述
使用 make(或 gomake) 可构建 even 包的目标文件 even.a, 而测试代码不能放入 GOFILES 环境变量, 因为在应用程序中, 无须提供测试代码, 如果包含在 GOFILES 环境变量中,gotest 将出现一个错误, 同时 gotest 可创建自包含测试代码的可执行文件, 并会在文件名中增加 _test.

现在可使用 go test(或 make test) 命令, 对 even 包进行测试, 由于例 13.5 给出的测试函数, 未调用 t.Log 或t.Fail, 所以它的测试结果为 PASS, 为了获取一个失败的测试结果, 可修改 TestEven 函数:
在这里插入图片描述
在这里插入图片描述

13.9 表驱动的测试

在编写测试程序时, 最好能使用一个数组, 同时列出测试的输入值和预定的测试结果, 这两个数值可合并成一个表元素, 而输入值和预定结果可成为一个完整的测试条件, 有时还需要给出其他的信息, 比如测试名称, 以便在输出结果中给出更多信息.

实际的测试只需给出所有表元素的迭代, 而每个表元素都将执行一类必要的测试, 以下代码将给出上述操作:
在这里插入图片描述
如果大量的测试函数都可使用上述方式进行写入, 也可将一个帮助函数 verify 写入实际的测试:
在这里插入图片描述

13.10 程序的性能数据

13.10.1 时间和内存的消耗

以下脚本可实现一个 xtime 命令:
在这里插入图片描述
其中分别是用户时间, 系统时间, 实时时间和最大的内存用量

13.10.2 gotest

如果代码中使用了 testing 包的评估功能, 则可在 gotest 运行时, 加入-cpuprofile 和-memprofile 标准选项, 它们可将 CPU 用量和内存用量, 写入一个特殊文件, 比如:
在这里插入图片描述
上述命令可编译和执行 x_test.go 文件中包含的测试, 并将 CPU 使用量信息写入到 prof.out 文件中.

13.10.3 pprof

对于一个独立程序 progexec 来说, 可导入 runtime/pprof 包, 使能轮廓功能, 该包可将运行时的性能数据, 以特定格式写入文件, 同时这类格式能被 pprof 可视化工具所支持, 所以在以下代码中, 可添加一些代码, 实现CPU 性能数据的输出:
在这里插入图片描述
上述代码中, 定义了一个名为 cpuprofile 标记, 调用 flag 包, 可实现命令行标记的解析, 如果在命令中设定了 cpuprofile 标记, 那么 CPU 的性能数据将记录到文件中 (os.Create 可创建一个同名文件, 上述的性能数据将写入该文件), 在上述代码中, 还需要实现 StopCPUProfile 最终调用, 以便在程序退出前, 可将缓冲数据全部更新到文件, 使用 defer 可保证 StopCPUProfile 的执行. 之后可在程序运行时, 添加标记, 如progexec -cpuprofile=progexec.prof, 之后可使用 gopprof 工具, 查看 CPU 的性能数据, 如gopprof progexec progexec.prof.gopprof 与 pprof(C++ 实现) 稍有不同, 它可提供更多的信息, 可参考页面 http://code.google.com/p/google-perftools/. 当 CPU 性能数据的采集被使能, Go 程序每秒可停止 100 次以上, 并记录当前执行的并发协程的堆栈中, 对应程序计数器的采样值. 在 gopprof 工具中, 还可使用以下命令:

• topN
可显示当前文件中, 最高的 N 个采样值, 比如 top5, 同时还将显示 10 个占用 CPU 资源最多的函数, 如
下:
在这里插入图片描述
• web(web [函数名])
该命令将以 SVG 格式, 写入一个性能数据的图表, 并显示在一个 web 浏览器中 ( 也可使用 gv 命令, 写
入 PostScript 格式的性能数据, 并能显示在 Ghostview 中, 为了使用上述命令, 必须安装 graphviz), 它
将在一个矩形 (通常被称为 bigger) 中, 显示不同的函数, 并使用箭头线, 来描述函数的调用.

• list [函数名] 或 weblist [函数名]
它可显示对应函数中代码行的列表, 第二列将给出该行代码的执行时间, 这可了解那些代码占用了更多
的资源.

基于内存的性能数据, 可了解到 runtime.mallocgc 函数 (可实现内存分配和周期性的垃圾收集) 占用了更多的资源, 同时还能知道到垃圾收集的频繁动作, 是由于内存的频繁分配所造成的. 以下的代码片段可添加到正确的位置上:
在这里插入图片描述
之后可使用内存标记, 来运行该程序, 比如progexec -memprofile=progexec.mprof, 再使用 gopprof 工具, 可查看内存的性能数据文件, 如gopprof progexec progexec.mprof. 当然在 gopprof 工具中, 也可使用 top5 等命令,同时内存分配的单位为 MB, 并会放置在输出显示的最顶层, 如下:
在这里插入图片描述
内存用量最大的函数将放置在第一行, 而第一列则给出了函数的内存用量, 而 gopprof 工具还可报告对象的分配次数:
在这里插入图片描述
在 web 应用中, 也需要标准 HTTP 接口的性能数据, 因此在一个 HTTP 服务器中, 可加入
在这里插入图片描述
在/debug/pprof/目录下, 可为一些 URL 配置处理器, 之后可运行 gopprof(只包含一个参数, 即 URL), 则可得到服务器的性能数据, 同时还可下载和检查这些性能数据,
在这里插入图片描述

在这里插入图片描述

发布了80 篇原创文章 · 获赞 10 · 访问量 19万+

猜你喜欢

转载自blog.csdn.net/osoon/article/details/103776674