Go 没有像 Java 和 .NET 那样的 try/catch 异常机制:不能执行抛异常操作。但是有一套 defer-panic-and-recover 机制。

Go 的设计者觉得 try/catch 机制的使用太泛滥了,而且从底层向更高的层级抛异常太耗费资源。他们给 Go 设计的机制也可以 “捕捉” 异常,但是更轻量,并且只应该作为(处理错误的)最后的手段。

Go 是怎么处理普通错误的呢?通过在函数和方法中返回错误对象作为它们的唯一或最后一个返回值——如果返回 nil,则没有错误发生——并且主调(calling)函数总是应该检查收到的错误。

处理错误并且在函数发生错误的地方给用户返回错误信息:照这样处理就算真的出了问题,你的程序也能继续运行并且通知给用户。panic and recover 是用来处理真正的异常(无法预测的错误)而不是普通的错误。

库函数通常必须返回某种错误提示给主调(calling)函数。

Go 检查和报告错误条件的惯有方式:

  • 产生错误的函数会返回两个变量,一个值和一个错误码;如果后者是nil就是成功,非 nil 就是发生了错误。
  • 为了防止发生错误时正在执行的函数(如果有必要的话甚至会是整个程序)被中止,在调用函数后必须检查错误。
  1. if value, err := pack1.Func1(param1); err != nil {
  2. fmt.Printf("Error %s in pack1.Func1 with parameter %v", err.Error(), param1)
  3. return // or: return err
  4. } else {
  5. // Process(value)
  6. }

为了更清晰的代码,应该总是使用包含错误值变量的 if 复合语句

错误处理

Go 有一个预先定义的 error 接口类型

  1. type error interface {
  2. Error() string
  3. }

错误值用来表示异常状态,errors 包中有一个 errorString 结构体实现了 error 接口。当程序处于错误状态时可以用 os.Exit(1) 来中止运行。

定义错误

任何时候当你需要一个新的错误类型,都可以用 errors(必须先 import)包的 errors.New 函数接收合适的错误信息来创建,像下面这样:

  1. err := errors.New("math - square root of negative number")

示例:

  1. // errors.go
  2. package main
  3. import (
  4. "errors"
  5. "fmt"
  6. )
  7. var errNotFound error = errors.New("Not found error")
  8. func main() {
  9. fmt.Printf("error: %v", errNotFound)
  10. }
  11. // error: Not found error

可以把它用于计算平方根函数的参数测试:

  1. func Sqrt(f float64) (float64, error) {
  2. if f < 0 {
  3. return 0, errors.New ("math - square root of negative number")
  4. }
  5. // implementation of Sqrt
  6. }

你可以像下面这样调用 Sqrt 函数:

  1. if f, err := Sqrt(-1); err != nil {
  2. fmt.Printf("Error: %s\n", err)
  3. }

由于 fmt.Printf 会自动调用 String() 方法 ,所以错误信息 “Error: math - square root of negative number” 会打印出来。通常(错误信息)都会有像 “Error:” 这样的前缀,所以你的错误信息不要以大写字母开头。

在大部分情况下自定义错误结构类型很有意义的,可以包含除了(低层级的)错误信息以外的其它有用信息,例如,正在进行的操作(打开文件等),全路径或名字。看下面例子中 os.Open 操作触发的 PathError 错误:

  1. // PathError records an error and the operation and file path that caused it.
  2. type PathError struct {
  3. Op string // "open", "unlink", etc.
  4. Path string // The associated file.
  5. Err error // Returned by the system call.
  6. }
  7. func (e *PathError) Error() string {
  8. return e.Op + " " + e.Path + ": "+ e.Err.Error()
  9. }

如果有不同错误条件可能发生,那么对实际的错误使用类型断言或类型判断(type-switch)是很有用的,并且可以根据错误场景做一些补救和恢复操作。

  1. // err != nil
  2. if e, ok := err.(*os.PathError); ok {
  3. // remedy situation
  4. }

或:

  1. switch err := err.(type) {
  2. case ParseError:
  3. PrintParseError(err)
  4. case PathError:
  5. PrintPathError(err)
  6. ...
  7. default:
  8. fmt.Printf("Not a special error, just %s\n", err)
  9. }

用 fmt 创建错误对象

通常你想要返回包含错误参数的更有信息量的字符串,例如:可以用 fmt.Errorf() 来实现:它和 fmt.Printf() 完全一样,接收一个或多个格式占位符的格式化字符串和相应数量的占位变量。和打印信息不同的是它用信息生成错误对象。

比如在前面的平方根例子中使用:

  1. if f < 0 {
  2. return 0, fmt.Errorf("math: square root of negative number %g", f)
  3. }

第二个例子:从命令行读取输入时,如果加了 help 标志,我们可以用有用的信息产生一个错误:

  1. if len(os.Args) > 1 && (os.Args[1] == "-h" || os.Args[1] == "--help") {
  2. err = fmt.Errorf("usage: %s infile.txt outfile.txt", filepath.Base(os.Args[0]))
  3. return
  4. }

运行时异常和 panic

当发生像数组下标越界或类型断言失败这样的运行错误时,Go 运行时会触发运行时 panic,伴随着程序的崩溃抛出一个 runtime.Error 接口类型的值。这个错误值有个 RuntimeError() 方法用于区别普通错误。

panic 可以直接从代码初始化:当错误条件(我们所测试的代码)很严苛且不可恢复,程序不能继续运行时,可以使用 panic 函数产生一个中止程序的运行时错误。panic 接收一个做任意类型的参数,通常是字符串,在程序死亡时被打印出来。Go 运行时负责中止程序并给出调试信息。

  1. package main
  2. import "fmt"
  3. func main() {
  4. fmt.Println("Starting the program")
  5. panic("A severe error occurred: stopping the program!")
  6. fmt.Println("Ending the program")
  7. }

输出如下:

  1. Starting the program
  2. panic: A severe error occurred: stopping the program!
  3. panic PC=0x4f3038
  4. runtime.panic+0x99 /go/src/pkg/runtime/proc.c:1032
  5. runtime.panic(0x442938, 0x4f08e8)
  6. main.main+0xa5 E:/Go/GoBoek/code examples/chapter 13/panic.go:8
  7. main.main()
  8. runtime.mainstart+0xf 386/asm.s:84
  9. runtime.mainstart()
  10. runtime.goexit /go/src/pkg/runtime/proc.c:148
  11. runtime.goexit()
  12. ---- Error run E:/Go/GoBoek/code examples/chapter 13/panic.exe with code Crashed
  13. ---- Program exited with code -1073741783

一个检查程序是否被已知用户启动的具体例子:

  1. var user = os.Getenv("USER")
  2. func check() {
  3. if user == "" {
  4. panic("Unknown user: no value for $USER")
  5. }
  6. }

可以在导入包的 init() 函数中检查这些。

当发生错误必须中止程序时,panic 可以用于错误处理模式:

  1. if err != nil {
  2. panic("ERROR occurred:" + err.Error())
  3. }

Go panicking:

在多层嵌套的函数调用中调用 panic,可以马上中止当前函数的执行,所有的 defer 语句都会保证执行并把控制权交还给接收到 panic 的函数调用者。这样向上冒泡直到最顶层,并执行(每层的) defer,在栈顶处程序崩溃,并在命令行中用传给 panic 的值报告错误情况:这个终止过程就是 panicking

不能随意地用 panic 中止程序,必须尽力补救错误让程序能继续执行。

从 panic 中恢复(Recover)

正如名字一样,这个recover内建函数被用于从 panic错误场景中恢复:让程序可以从 panicking 重新获得控制权,停止终止过程进而恢复正常执行

recover 只能在 defer 修饰的函数中使用:用于取得 panic 调用中传递过来的错误值,如果是正常执行,调用 recover 会返回 nil,且没有其它效果。

panic 会导致栈被展开直到 defer 修饰的 recover() 被调用或者程序中止。

下面例子中的 protect 函数调用函数参数 g 来保护调用者防止从 g 中抛出的运行时 panic,并展示 panic 中的信息:

  1. func protect(g func()) {
  2. defer func() {
  3. log.Println("done")
  4. // Println executes normally even if there is a panic
  5. if err := recover(); err != nil {
  6. log.Printf("run time panic: %v", err)
  7. }
  8. }()
  9. log.Println("start")
  10. g() // possible runtime-error
  11. }

这是一个展示 panic,defer 和 recover 怎么结合使用的完整例子:

  1. // panic_recover.go
  2. package main
  3. import (
  4. "fmt"
  5. )
  6. func badCall() {
  7. panic("bad end")
  8. }
  9. func test() {
  10. defer func() {
  11. if e := recover(); e != nil {
  12. fmt.Printf("Panicing %s\r\n", e)
  13. }
  14. }()
  15. badCall()
  16. fmt.Printf("After bad call\r\n") // <-- wordt niet bereikt
  17. }
  18. func main() {
  19. fmt.Printf("Calling test\r\n")
  20. test()
  21. fmt.Printf("Test completed\r\n")
  22. }

输出:

  1. Calling test
  2. Panicing bad end
  3. Test completed

直观地看,抛出panic后,直接进入defer 函数内,如果有recover()就继续执行,没有直接抛错

defer-panic-recover 在某种意义上也是一种像 iffor 这样的控制流机制。

自定义包中的错误处理和 panicking

这是所有自定义包实现者应该遵守的最佳实践:

1)在包内部,总是应该从 panicrecover:不允许显式的超出包范围的 panic()

2)向包的调用者返回错误值(而不是 panic

在包内部,特别是在非导出函数中有很深层次的嵌套调用时,将 panic 转换成 error 来告诉调用方为何出错,是很实用的(且提高了代码可读性)。

下面的代码则很好地阐述了这一点。

  1. // parse.go
  2. package parse
  3. import (
  4. "fmt"
  5. "strings"
  6. "strconv"
  7. )
  8. // A ParseError indicates an error in converting a word into an integer.
  9. type ParseError struct {
  10. Index int // The index into the space-separated list of words.
  11. Word string // The word that generated the parse error.
  12. Err error // The raw error that precipitated this error, if any.
  13. }
  14. // String returns a human-readable error message.
  15. func (e *ParseError) String() string {
  16. return fmt.Sprintf("pkg parse: error parsing %q as int", e.Word)
  17. }
  18. // Parse parses the space-separated words in in put as integers.
  19. func Parse(input string) (numbers []int, err error) {
  20. defer func() {
  21. if r := recover(); r != nil {
  22. var ok bool
  23. err, ok = r.(error)
  24. if !ok {
  25. err = fmt.Errorf("pkg: %v", r)
  26. }
  27. }
  28. }()
  29. fields := strings.Fields(input)
  30. numbers = fields2numbers(fields)
  31. return
  32. }
  33. func fields2numbers(fields []string) (numbers []int) {
  34. if len(fields) == 0 {
  35. panic("no words to parse")
  36. }
  37. for idx, field := range fields {
  38. num, err := strconv.Atoi(field)
  39. if err != nil {
  40. panic(&ParseError{idx, field, err})
  41. }
  42. numbers = append(numbers, num)
  43. }
  44. return
  45. }
  1. // panic_package.go
  2. package main
  3. import (
  4. "fmt"
  5. "./parse/parse"
  6. )
  7. func main() {
  8. var examples = []string{
  9. "1 2 3 4 5",
  10. "100 50 25 12.5 6.25",
  11. "2 + 2 = 4",
  12. "1st class",
  13. "",
  14. }
  15. for _, ex := range examples {
  16. fmt.Printf("Parsing %q:\n ", ex)
  17. nums, err := parse.Parse(ex)
  18. if err != nil {
  19. fmt.Println(err) // here String() method from ParseError is used
  20. continue
  21. }
  22. fmt.Println(nums)
  23. }
  24. }

输出:

  1. Parsing "1 2 3 4 5":
  2. [1 2 3 4 5]
  3. Parsing "100 50 25 12.5 6.25":
  4. pkg: pkg parse: error parsing "12.5" as int
  5. Parsing "2 + 2 = 4":
  6. pkg: pkg parse: error parsing "+" as int
  7. Parsing "1st class":
  8. pkg: pkg parse: error parsing "1st" as int
  9. Parsing "":
  10. pkg: no words to parse

『GoLang』错误处理的更多相关文章

  1. 『Golang』Martini框架入门

    本文介绍golang中的优秀web开发框架martini! 序 Martini框架是使用Go语言作为开发语言的一个强力的快速构建模块化web应用与服务的开发框架.Martini是一个专门用来处理Web ...

  2. 『Golang』跨平台TUI(基于文字的用户界面)库Terbox-Go文档翻译

    原文 package termbox import "github.com/nsf/termbox-go" termbox-go 是一个用于创建跨平台TUI(基于文本的用户界面)的 ...

  3. 『Golang』MongoDB在Golang中的使用(mgo包)

    有关在Golang中使用mho进行MongoDB操作的最简单的例子.

  4. 『Golang』在Golang中使用json

    由于要开发一个小型的web应用,而web应用大部分都会使用json作为数据传输的格式,所以有了这篇文章. 包引用 import ( "encoding/json" "gi ...

  5. 『Golang』—— 标准库之 os

    Golang 的 os 库基本承袭 Unix 下 C 语言的用法 path 库: func Base(path string) string //取文件名,不含目录部分 func Dir(path s ...

  6. 『GoLang』fmt包的使用

    目录 1. fmt 包初识 2. 格式化 verb 应用 2.1 通用 2.2 布尔值 2.3 整数 2.4 浮点数与复数 2.5 字符串和 []byte 2.6 指针 2.7 其他 flag 2.8 ...

  7. 『GoLang』string及其相关操作

    目录 1. 字符串简介 2. 字符串的拼接 3. 有关 string 的常用处理 3.1 strings 包 3.1.1 判断两个 utf-8 编码字符串是否相同 3.1.2 判断字符串 str 是否 ...

  8. 『GoLang』面向对象

    我们总结一下前面看到的:Go 没有类,而是松耦合的类型.方法对接口的实现. 面向对象语言最重要的三个方面分别是:封装,继承和多态,在 Go 中它们是怎样表现的呢? Go实现面向对象的两个关键是stru ...

  9. 『GoLang』接口

    接口是什么 Go 语言不是一种 "传统" 的面向对象编程语言:它里面没有类和继承的概念. 但是 Go 语言里有非常灵活的 接口 概念,通过它可以实现很多面向对象的特性.接口提供了一 ...

随机推荐

  1. 高性能 C++ HTTP 客户端原理与实现

    一.什么是Http Client Http协议,是全互联网共同的语言,而Http Client,可以说是我们需要从互联网世界获取数据的最基本方法,它本质上是一个URL到一个网页的转换过程.而有了基本的 ...

  2. 题解 Prime

    传送门 考场上魔改了一下线性筛,觉得要筛到 \(\frac{R}{2}\) 就没让它跑 其实正解就是这样,只不过由于接下来类似埃氏筛的过程只要筛到根号就行了 线性筛有的时候其实并不需要筛到 \(\fr ...

  3. 本文详细阐述如何用C#创建COM组件,并能用VC6.0等调用。

    本文详细阐述如何用C#创建COM组件,并能用VC6.0等调用. 附:本文适用任何VS系列工具. 在用C#创建COM组件时,一定要记住以下几点: 1.所要导出的类必须为公有: 2.所有属性.方法也必须为 ...

  4. java实用资料

    1.怎么构造一个线程安全的hashmap?用reentrantreadwritelock2.线程是怎么处理二个以上的对象同时处理一个全局变量 3.读文件为啥不用字符流 4.请求鉴定,各种错误码502- ...

  5. WPF 勾选划线

    最近项目需要一个左右侧一对多的划线功能 我们先来看一下效果秃: 主要功能: 支持动态添加 支持复选 支持修改颜色 支持动态宽度 主要实现:事件的传递 应用场景:购物互选,食品搭配,角色互选 数据源 左 ...

  6. 网络视频m3u8解密及ts文件合并

    网络视频m3u8解密及ts文件合并 参考了两篇博客: https://blog.csdn.net/weixin_41624645/article/details/95939510 https://bl ...

  7. js函数和封装

    $就是jquery对象,$()就是jQuery(),在里面可以传参数,作用就是获取元素 js对象与jQuery对象的区别:jQuery对象是一个数组,jQuery对象转为js对象:[0] 取第一个即可 ...

  8. nginx 开启,关闭,重启

    2021-08-191. 启动 # 判断配置文件是否正确 cd /usr/local/nginx/sbin ./nginx -t # 启动 cd usr/local/nginx/sbin ./ngin ...

  9. NOIP模拟「random·string·queen」

    T1:random   我又来白剽博客了:   详细证明请看土哥   土哥写的不是很详细,我在这里详细写一下:   首先,对于f[n]的式子:   加一是那一个对的贡献,大C是选其余的几个数,\(2^ ...

  10. Python - 面向对象编程 - __call__()

    __call()__ 的作用 使得类实例对象可以像普通函数那样被调用 实际栗子 from typing import Callable class PoloBlog: def __init__(sel ...