本文最初发表在我的个人博客,查看原文,获得更好的阅读体验


一 错误

1.1 error类型

按照约定,Go中的错误类型为error,这是一个内建接口,nil值表示没有错误:

type error interface {Error() string
}

我们可以很方便的自定义一个错误类型:

package mainimport ("fmt"
)func main() {e := MyError{"This is a custom Error Type."}fmt.Println(e.Error())v1, err := divide(10, 2)if err == nil {fmt.Println(v1)}if v2, err := divide(5, 0); err != nil {fmt.Println(err)} else {fmt.Println(v2)}
}// 自定义错误类型
type MyError struct {msg string
}func (e *MyError) Error() string {return e.msg
}// 取整除法
func divide(a1, a2 int) (int, error) {if a2 == 0 {return 0, &MyError{"整数相除,除数不能为零"}}return a1 / a2, nil
}

上述divide函数会返回一个error值,调用方可以根据这个错误值来判断如何处理结果。这种用法在Go中是一种惯用法,尤其在编写一些函数库之类的功能时。例如标准库os中的打开文件的Open函数定义如下:

// Open opens the named file for reading. If successful, methods on
// the returned file can be used for reading; the associated file
// descriptor has mode O_RDONLY.
// If there is an error, it will be of type *PathError.
func Open(name string) (*File, error) {return OpenFile(name, O_RDONLY, 0)
}

该函数返回的具体错误类型为PathError

// PathError records an error and the operation and file path that caused it.
type PathError struct {Op   stringPath stringErr  error
}func (e *PathError) Error() string { return e.Op + " " + e.Path + ": " + e.Err.Error() }

该错误详细的描述了引发错误的操作以及相关文件路径和错误描述信息。

1.2 其他错误类型

除此之外,标准库中还有许多其他预定义的错误类型,它们都直接或间接的实现(或内嵌)了error接口。例如:

runtime.Error       // 表示运行时错误的Error接口类型
net.Error           // 表示网络错误的Error接口类型
go/types.Error      // 表示类型检查错误的Error结构类型
html/template.Error // 表示html模板转义期间遇到的问题(结构类型)
os/exec             // 当文件不是一个可执行文件时返回的错误(结构类型)

另外,标准库中的errors包提供了一个函数可方便地返回一个error实例:

// New 函数返回格式为给定文本的错误
func New(text string) error

它的具体实现如下:

// errors 包实现了操作错误的函数
package errors// New 函数返回格式为给定文本的错误
func New(text string) error {return &errorString{text}
}// errorString 是 error 的一个简单实现(注意是私有的)
type errorString struct {s string
}func (e *errorString) Error() string {return e.s
}

示例:

package mainimport ("errors""fmt"
)func main() {fmt.Println(errors.New("这是一条错误信息"))
}

如果上述错误描述过于简单,还可以使用fmt包中的Errorf函数:

// Errorf根据指定的格式进行格式化参数,并返回满足error接口的字符串
func Errorf(format string, a ...interface{}) error {return errors.New(Sprintf(format, a...))
}

该函数允许我们使用软件包的格式化功能来创建描述性错误消息:

package mainimport ("fmt"
)func main() {const name, id = "bimmler", 17err := fmt.Errorf("user %q (id %d) not found", name, id)if err != nil {fmt.Print(err)}
}

通常,以上两种方法能满足绝大多数错误场景。如果仍然不够,正如本文开头所讲,你可以自定义任意的错误类型。

二 Panic(恐慌)

内建函数panic可以产生一个运行时错误,一旦调用该函数,当前goroutine就会停止正常的执行流程。这种情况一般发生在一些重要参数缺失的检查时,因为如果缺失了这些参数,将导致程序不能正常运行,故相比让程序继续运行(也可能根本就没法正常运行),不如让它及时终止。

func panic(v interface{})

该函数接受一个任意类型的实参(一般为字符串),并在程序终止时打印。

package mainfunc main() {panic("运行出错了。")
}

另一类使用场景:

package mainimport ("fmt""os"
)func main() {fmt.Println("wait for init...")
}var user = os.Getenv("USER")func init() {// 检查必要变量等if user == "" {panic("no value for $USER")}
}

一般情况下,我们应避免使用panic,尤其是在库函数中。

panic被调用后(包括不明确的运行时错误,例如数组或切片索引越、类型断言失败)等,程序将立刻终止当前函数的执行,并开始回溯goroutine的栈,运行任何被推迟的函数。若回溯到达goroutine栈的顶端,程序就会终止。

假设函数F调用了panic,则F的正常执行将立即停止。F中任何被推迟的函数将依次执行,然后F返回到调用处。对于调用者G,此时好像也在调用panic函数一样,执行到此立即停止,并开始回溯所有被G推迟的函数。就这样一直回溯,直到该goroutine中的所有函数都停止,此时,程序终止,并报告错误信息,包括传给panic的参数。

当然,我们还可以使用内建函数recover进行恢复,夺回goroutine的控制权,继续往下看。

我们在defer语句一文中提到过,defer栈是以LIFO的顺序执行的。

三 Recover(恢复)

内建函数recover可以让发生panickinggoroutine恢复正常运行。在一个被推迟的函数中执行recover可以终止panic的产生的终止回溯调用。注意必须是直接在被推迟的函数中。如果不是在推迟函数中(或间接)调用该函数,则不会发生任何作用,将返回nil。如果程序没有发生panicpanic的参数为nil,则recover的返回值也为nil

func recover() interface{}

以下示例展示了panic和recover的工作机制:

package mainimport "fmt"func main() {f()fmt.Println("从 f() 中正常返回。")
}func f() {defer func() {if r := recover(); r != nil {fmt.Println("从 f() 中正常恢复。", r)}}()fmt.Println("开始调用函数 g()。。。")g(0)fmt.Println("从 g() 中正常返回。")
}func g(i int) {if i > 3 {fmt.Println("Panicking!")panic(fmt.Sprintf("%v", i))}defer fmt.Println("函数g()中推迟的调用", i)fmt.Println("函数g()中的打印", i)g(i + 1)
}func h() {fmt.Println("hello")
}

看一个effective_go中的例子:

在服务器中终止失败的goroutine而无需杀死其它正在执行的goroutine

func server(workChan <-chan *Work) {for work := range workChan {go safelyDo(work)}
}func safelyDo(work *Work) {defer func() {if err := recover(); err != nil {log.Println("work failed:", err)}}()do(work)
}

通过恰当地使用恢复模式,do函数(及其调用的任何代码)可通过调用panic来避免更坏的结果。我们可以利用这种思想来简化复杂软件中的错误处理。

再看一个regexp包的理想化版本,它会以局部的错误类型调用panic来报告解析错误。以下是一个Error类型,一个error方法和一个Compile函数的定义:

//  Error 是解析错误的类型,它满足 error 接口。
type Error string
func (e Error) Error() string {return string(e)
}// error 是 *Regexp 的方法,它通过用一个 Error 触发Panic来报告解析错误。
func (regexp *Regexp) error(err string) {panic(Error(err))
}// Compile 返回该正则表达式解析后的表示。
func Compile(str string) (regexp *Regexp, err error) {regexp = new(Regexp)// 如果有解析错误,doParse会产生panicdefer func() {if e := recover(); e != nil {regexp = nil    // 清理返回值err = e.(Error) // 若它不是解析错误,将重新触发Panic。}}()return regexp.doParse(str), nil
}

如果doParse触发了panic,恢复块会将返回值设为nil—被推迟的函数能够修改已命名的返回值。在err的赋值过程中,我们将通过断言它是否拥有局部类型Error来检查它。若它没有,类型断言将会失败,此时会产生运行时错误,并继续栈的回溯,仿佛一切从未中断过一样。该检查意味着若发生了一些像索引越界之类的意外,那么即便我们使用了panicrecover来处理解析错误,代码仍然会失败。

通过适当的错误处理,error方法(由于它是个绑定到具体类型的方法,因此即便它与内建的error类型名字相同也没有关系)能让报告解析错误变得更容易,而无需担心手动处理回溯的解析栈:

if pos == 0 {re.error("'*' illegal at start of expression")
}

尽管这种模式很有用,但它应当仅在包内使用。Parse会将其内部的panic调用转为error值,它并不会向调用者暴露出panic。这是个值得遵守的良好规则。

另外,这种重新触发panic的惯用法会在产生实际错误时改变panic的值。然而,不管是原始的还是新的错误都会在崩溃报告中显示,因此问题的根源仍然是可见的。这种简单的重新触发panic的模型已经够用了,毕竟它只是一次崩溃。但若你只想显示原始的值,也可以多写一点代码来过滤掉不需要的问题,然后用原始值再次触发panic

参考:
https://golang.org/doc/effective_go.html#errors
https://golang.org/pkg/builtin/#error
https://blog.golang.org/defer-panic-and-recover

Go语言学习 二十三 错误处理和运行时恐慌(Panic)相关推荐

  1. OpenCV学习(二十三) :模板匹配:matchTemplate(),minMaxLoc()

    OpenCV学习(二十三) :模板匹配:matchTemplate() 1.概述 模板匹配是一种最原始.最基本的模式识别方法,研究某一特定对象物的图案位于图像的什么地方,进而识别对象物,这就是一个匹配 ...

  2. R语言学习二——工具的使用

    R语言学习(二) 本章学习R语言相关开发工具的使用: 软件下载 软件安装 RStudio的使用 R扩展包的安装与载入 容易遇到的问题 一.软件下载(RStudio) Rstudio下载地址 选择免费版 ...

  3. Swift语言学习(二)

    原文链接:http://www.ioswift.org/ 4.0.Swift指南 以上章节主要从整体上介绍了 Swift 的相关知识,从本章开始,我们一步一步学习 Swift ,正式开启 Swift ...

  4. C语言试题二十三之编写一个函数void function(int tt[m][n],int pp[n]),tt指向一个m行n列的二维函数组,求出二维函数组每列中最小元素,并依次放入pp所指定一维数组中

    1. 题目 请编写一个函数void function(int tt[m][n],int pp[n]),tt指向一个m行n列的二维函数组,求出二维函数组每列中最小元素,并依次放入pp所指定一维数组中.二 ...

  5. Go语言学习 二十一 内嵌

    本文最初发表在我的个人博客,查看原文,获得更好的阅读体验 在像Java这种语言中,有子类(或者继承)的概念,通过继承复用已有的功能或属性,与继承不同,Go中使用组合的方式来完成已有实现的复用,这种做法 ...

  6. Go语言学习二 语言结构 基础语法 数据类型

    Go 语言结构 由 youj 创建, 最后一次修改 2015-09-08 Go 语言结构 在我们开始学习 GO 编程语言的基础构建模块前,让我们先来了解 Go 语言最简单程序的结构. Go Hello ...

  7. C语言学习二:VS的使用

    Visual Studio环境 VS是Visual Studio,它是微软提供的一个工具集,由各种各样的工具组成.VS可以支持C/C++.VB.JAVA.C#编程.然了一次只能支持一种编程方式.在VS ...

  8. SQL语言之DQL语言学习(二)条件查询

    查询后并拼接字段 主要利用Concat( , )拼接函数; SELECT CONCAT(last_name,first_name) AS 姓名 from employees; 条件查询语法 selec ...

  9. Swift5.1 语言指南(二十三) 协议

    ★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★ ➤微信公众号:山青咏芝(shanqingyongzhi) ➤博客园地址:山青咏芝(https://www.cnblog ...

最新文章

  1. 新基建数据中心如何建?附建设导则
  2. python生成随机密码
  3. php curl title,PHP中使用CURL获取页面title例子
  4. Android学习总结00之废话
  5. 我的《野蛮生长》书摘
  6. idea中拉取项目时 没有文件_idea编译器中maven项目获取路径的方法
  7. SpringBoot部署Jar文件,瘦身优化指南!
  8. VUE3封装axios网络请求
  9. 怎么做应力应变曲线_金属薄板塑性应变比ISO 10113:2020 解读
  10. mysql 创建外键语句,MySQL 创建主键,外键和复合主键的语句 | 很文博客
  11. 记录一次elastic-job分片查询及基础概念理解
  12. cei()、linspace()、arrange()、full()、eye()、empty()、random()
  13. 全国最大SLAM开发者学习交流社区 欢迎加入
  14. lintcode(507)摆动排序 II
  15. 如何在表格里做计算机统计表,如何运用Excel编制统计表并做一般数据分析?-excel统计怎么做,最简单的统计表格怎么做...
  16. ASP网页HTTP 错误 404.3 - Not Found解决方案
  17. 思成五笔的通俗易懂讲解
  18. 表情分析计算机,利用深度学习和计算机视觉进行面部表情分析
  19. Installshield 安装包安装过程中遇到的报错(一)
  20. 微信小程序背景图片不显示

热门文章

  1. 如何精进Excel水平?从邮件小工具讲起
  2. html+css+气泡,CSS气泡
  3. 这些你必须知道的 Linux 技能
  4. 递归函数的简单应用-第五个学生的年龄
  5. 阿里云服务器安装mongodb
  6. 软件测试的正向思维,反向思维
  7. linux格式化光盘找不到介质,Linux挂载光盘的问题解决方案(mount: you must specify the filesystemnbs...
  8. 刷脸开门上班取外卖等都会无处不在
  9. 让我们旋转跳跃不停歇~~~当3D打印遇上八音盒!(二)
  10. 5款小巧有趣的微信小程序,个个让你心花怒放!