任何一门开发语言中,错误和异常处理都是不可回避的问题,Go 中主要通过 error 和 panic 分别表示错误和异常,并提供了较为简洁的错误异常处理机制。作为一门并发性能优越的语言,Go 虽然降低了协程并发开发的难度,但也存在一些并发陷阱,这就需要我们在开发时额外注意。

1. Errors are values

错误处理是每个开发人员都需要面对的问题,在大多数的编程语言中比如Java、python,都是通过try-catch 的方式对可能出现错误的代码块进行包装:程序运行 try 中代码,如果 try 中的代码运行出错,程序将会立即跳转到 catch 中执行异常处理逻辑。

#!/usr/bin/python
# -*- coding: UTF-8 -*-try:fh = open("testfile", "w")fh.write("这是一个测试文件,用于测试异常!!")
except IOError:print "Error: 没有找到文件或读取文件失败"
else:print "内容写入文件成功"fh.close()
  public static void main(String args[]){int a[] = new int[2];try{System.out.println("Access element three :" + a[3]);}catch(ArrayIndexOutOfBoundsException e){System.out.println("Exception thrown  :" + e);}finally{System.out.println("The finally statement is executed");}}

与其他的编程语言不同,Go 中倡导“Errors are values!”的处理思想,它将 error 作为一个返回值,来迫使调用者对 error 进行处理或者忽略。于是,在代码中我们将会编写大量的 if 判断语句对 error 进行判断,如下所示:

result, err:= Sqrt(-1)
if err != nil {fmt.Println(err)
}
// do other thing

当项目的代码快速增长起来时,我们会发现代码中到处都是类似 err != nil 的判断片段。虽然这会使代码变得很烦琐,但是这种设计和约定也会鼓励开发人员明确检查和确定错误发生的位置。

在 Go 中,error 接口定义如下:

type error interface {Error() string
}

最常用的 error 实现是 Go 标准库 errors 包中内置的 errorString,它是一个仅包含错误信息的error 实现,可以通过 errors.New 和 fmt.Errorf 函数创建。内置的 error 接口使得开发人员可以为错误添加任何所需的信息,error 可以是实现 Error() 方法的任何类型,比如我们可以为错误添加错误码和调用栈信息,如下所示:

type Error struct {Msg stringCode int32St []uintptr // 调用栈
}
// 获取调用栈信息
func callers() []uintptr {var pcs [32]uintptrn := runtime.Callers(3, pcs[:])st := pcs[0:n]return st
}
func New(code int32, msg string) error {return &Error{Code: int32(code),Msg:  msg,St: callers(),}
}
func (e *Error) Error() string {if e == nil {return "OK"}return fmt.Sprintf("code:%d, msg:%s", e.Code, e.Msg)
}

通过断言的方式可以将 error 转化为特定的类型从而进行特异化处理,如下所示:

if e, ok := err.(*Error); ok {// 取出堆栈信息进行处理st := e.st// ....
}else {// 其他错误处理
}

在 Go 1.13 版本之后,errors 包中添加了 errors.Is 和 errors.As 函数:errors.Is 方法用来比较两个 error 是否相等,而 errors.As 函数用来判断 error 是否为特定类型。

由于 error 是一个值,因此我们可以对其进行编程,简化 Go 错误处理的重复代码。在一些管道和循环的代码中,只要其中一次处理出现错误,就应该退出本次管道或者循环。寻常的做法是在每次迭代都检查错误,但为了让管道和循环的操作显得更加自然,我们可以将 error 封装到独立的方法或者变量中返回,以避免错误处理掩盖控制流程,如 gorm 中的 DB 设计所示:

err := DB.Where(queryString, queryValue...).Table("table_name").Updates(map[string]interface{}{...}).Error
if err != nil{// 错误处理逻辑
}

这里 error 是从 gorm.DB 的 Error 成员变量中获取的。在数据库请求执行结束之后,程序才从 DB 中获取执行错误,这样的写法使得错误处理不会中断执行流程。但需要注意的是,无论如何简化 error 的设计,程序都要检查和处理错误,错误是无法避免的。

2.defer、panic 和 recover

错误一般是一些开发人员“意料之内”的错误,比如获取数据库连接失败等,这些都是在 Go 中通过 error 表达并可控。但当程序出现异常,如数组访问越界这类“意料之外”的错误时,它能够导致程序运行崩溃,此时就需要开发人员捕获异常并恢复程序的正常运行流程。

接下来我们就介绍 defer、panic 和 recover 如何组合恢复运行时执行异常的 Go 程序。

defer 是 Go 中提供的一种延迟执行机制,每次执行 defer,都会将对应的函数压入栈中。在函数返回或者 panic 异常结束时,Go 会依次从栈中取出延迟函数执行。

在编程的时候,经常需要打开一些资源,比如数据库连接、文件等,在资源使用完成之后需要释放,不然有可能会造成资源泄漏。这个时候,我们可以通过 defer 语句在函数执行完之后,自动释放资源,避免在每个函数返回之前手动释放资源,减少冗余代码。

defer 有三个比较重要的特点。第一个是按照调用 defer 的逆序执行,即后调用的在函数退出时先执行,后进先出。如下例子所示:

func main()  {defer fmt.Println("first")defer fmt.Println("middle")defer fmt.Println("last")fmt.Println("test")
}

结果为:

test
last
middle
first

第二个特点是 defer 被定义时,参数变量会被立即解析,传递参数的值拷贝。在函数内使用的变量其实是对外部变量的一个拷贝,在函数体内,对变量更改也不会影响外部变量,如下所示:

func main()  {i := 10defer fmt.Printf("defer i is %d\n", i)i = 20fmt.Printf("current i is %d\n", i)
}

结果为:

current i is 20
defer i is 10

然而当 defer 以闭包的方式引用外部变量时,则会在延迟函数真正执行的时候,根据整个上下文确定当前的值,如下示例代码:

func main()  {for i := 0; i < 5; i++ {defer func() {fmt.Println(i)}()}
}

结果为:

5
5
5
5
5

上述例子为了演示简单,在 for 循环中使用了 defer,但在日常开发中,我建议你还是不要在循环中使用 defer。因为相较于直接调用,defer 的执行存在着额外的开销,例如 defer 会对其后需要的参数进行内存拷贝,还会对 defer 结构进行压栈出栈操作。因此,在循环中使用 defer 可能会带来较大的性能开销。

defer 的第三个特点是可以读取并修改函数的返回值,如下面的例子所示:

func main()  {fmt.Println(test())
}
func test() (i int) {defer func() { i++ }()return 1
}

结果为:

2

这是因为对于命名返回值,defer 和 return 的执行顺序如下:

将 1 赋给 i;
执行 i++;
返回 i 作为函数返回值。

defer 的内部实现为一个延迟调用链表,如下图所示:
其中,g 代表 goroutine 的数据结构。每个 goroutine 中都有一个 _defer 链表,当代码中遇到 defer 关键字时,Go 都会将 defer 相关的函数和参数封装到 _defer 结构体中,然后将其注册到当前 goroutine 的 _defer 链表的表头。在当前函数执行完毕之后,Go 会从 goroutine 的 _defer 链表头部取出来注册的 defer 执行并返回。

_defer 结构体中存储 defer 执行相关的信息,定义如下所示:

type _defer struct {siz       int32 // 参数与结果内存大小started   bool heap      bool // 是否在堆上分配openDefer bool //是否经过开放编码优化sp        uintptr // 栈指针pc        uintptr // 调用方的程序计数器fn        *funcval // defer 传入的函数_panic    *_panic link      *_defer // 下一个 _defer
}

panic 是一个内置函数,用于抛出程序执行的异常。它会终止其后将要执行的代码,并依次逆序执行 panic 所在函数可能存在的 defer 函数列表;然后返回该函数的调用方,如果函数的调用方中也有 defer 函数列表,也将被逆序执行,执行结束后再返回到上一层调用方,直到返回当前 goroutine 中的所有函数为止,最后报告异常,程序崩溃退出。异常可以直接通过 panic 函数调用抛出,也可能是因为运行时错误而引发,比如访问了空指针等。

而recover 内置函数可用于捕获 panic,重新恢复程序正常执行流程,但是 recover 函数只有在 defer 内部使用才有效。如下面例子所示:

func main() {err := panicAndReturnErr()if err != nil{fmt.Printf("err is %+v\n", err)}fmt.Println("returned normally from panicAndReturnErr")
}
func panicAndReturnErr() (err error){defer func() {// 从 panic 中恢复if e := recover(); e != nil {// 打印栈信息buf := make([]byte, 1024)buf = buf[:runtime.Stack(buf, false)]err = fmt.Errorf("[PANIC]%v\n%s\n", e, buf)}}()fmt.Println("panic begin")panic("panic this game")fmt.Println("panic over")return nil
}

执行结果为:

panic begin
err is [PANIC]panic this game
goroutine 1 [running]:
main.panicAndReturnErr.func1(0xc000062f08)/Users/apple/Desktop/example/defer_example.go:21 +0xa1
panic(0x10ad640, 0x10eb360)/usr/local/go/src/runtime/panic.go:969 +0x166
main.panicAndReturnErr(0x0, 0x0)/Users/apple/Desktop/example/defer_example.go:26 +0xc2
main.main()/Users/apple/Desktop/example/defer_example.go:10 +0x26
returned normally from panicAndReturnErr

从这个执行结果可以看出,panicAndReturnErr 函数在 panic 之后将会执行 defer 定义的延迟函数,恢复程序的正常执行逻辑。在上述例子中,我们在 defer 函数中使用 recover 函数帮助程序从 panic 中恢复过来,并获取异常堆栈信息组成 error 返回调用方。panicAndReturnErr 从 panic 中恢复后将直接返回,不会执行函数中 panic 后的其他代码。

在日常开发中,对于可能出现执行异常的函数,如数组越界、操作空指针等,在函数中定义一个使用 recover 函数的 defer 延迟函数,有利提高程序执行的健壮性,避免程序运行时异常崩溃。

3.常见的并发陷阱

最后我们再来介绍与 Go 并发相关的几个小技巧,帮助你规避 Go 并发开发的一些陷阱。首先是循环并发时闭包传递参数的问题,如下错误例子所示:

func main()  {for i := 0 ; i < 5 ; i++{go func() {fmt.Println("current i is ", i)}()}time.Sleep(time.Second)
}

这段代码极有可能的输出为:

current i is 5
current i is 5
current i is 5
current i is 5
current i is 5

这是因为 i 使用的地址空间在循环中被复用,在 goroutine 执行时,i 的值可能在被主 goroutine 修改,而此时其他 goroutine 也在读取使用,从而导致了并发错误。针对这种错误可以通过复制拷贝或者传参拷贝的方式规避,如下所示:

func main()  {for i := 0 ; i < 5 ; i++{go func(v int) {fmt.Println("current i is", v)}(i)}time.Sleep(time.Second)
}

前面介绍 panic 时我们了解到 panic 异常的出现会导致 Go 程序的崩溃。但其实即使 panic 是出现在其他启动的子 goroutine 中,也会导致 Go 程序的崩溃退出,同时 panic 只能捕获 goroutine 自身的异常,因此对于每个启动的 goroutine,都需要在入口处捕获 panic,并尝试打印堆栈信息并进行异常处理,从而避免子 goroutine 的 panic 导致整个程序的崩溃退出。如下面的例子所示:

func RecoverPanic() {// 从 panic 中恢复并打印栈信息if e := recover(); e != nil {buf := make([]byte, 1024)buf = buf[:runtime.Stack(buf, false)]fmt.Printf("[PANIC]%v\n%s\n", e, buf)}
}
func main() {for i:= 0 ; i < 5 ; i++{go func() {// defer 注册 panic 捕获函数defer RecoverPanic()dothing()}()}
}

最后一个技巧是要善于结合使用 select、timer 和 context 进行超时控制。在 goroutine 中进行一些耗时较长的操作,最好都加上超时 timer,在并发的时候也要传递 context,这样在取消的时候就不会有遗漏,进而达到回收 goroutine 的目的,避免内存泄漏的发生。如下面的例子所示,通过 select 同时监听任务和定时器状态,在定时器到达而任务未完成之时,提前结束任务,清理资源并返回。

select {// do logic process
case msg <- input:....
// has been canceled
case <-ctx.Done():// ...资源清理return
// 2 second timeout
case <-time.After(time.Second * 2)// ...资源清理 return
default:
}

最后,对于 Go 中的错误处理和并发技巧,你还有哪些经验?欢迎在留言。

浅谈Go 错误异常处理方式和并发陷进相关推荐

  1. 浅谈尾递归的优化方式

    在上文<尾递归与Continuation>里,我们谈到了尾递归的概念和示例,不过有些朋友对于尾递归的功效依然有所怀疑.因此现在,我再简单讲解一下尾递归的优化原理,希望能给大家以一定理性认识 ...

  2. 浅谈JavaScript错误

    本文主要从前端开发者的角度谈一谈大多数前端开发者都会遇到的js错误,对错误产生的原因.发生阶段,以及如何应对错误进行分析.归纳和总结,希望得到一些有益的结论用来指导日常开发工作. 概念辨析 错误(Er ...

  3. 老派程序员浅谈Python的异常处理

    @本文来源于公众号:csdn2299,喜欢可以关注公众号 程序员学府 文章目录 一.与python异常相关的关键字 二.python中的异常类型 三.基本方式: 三.手动使用raise引发异常 Pyt ...

  4. 浅谈ddos的测试方式

    DOS(denial of service--拒绝服务)攻击的目的是使服务正常功能不可用.不像其他类型的攻击的目的是获取敏感信息,Dos攻击是不会威胁到敏感信息而是使合法用户不能使用服务.有时候Dos ...

  5. mysql账号认证_浅谈MySQL用户账号认证方式

    为了有效控制数据库用户的访问权限,在MySQL数据库中创建了一个新用户,但使用刚创建的用户和密码却发现连接不了MySQL数据库,通过查看官网手册及<MySQL技术内幕>一书,才逐渐熟悉My ...

  6. 以前一款半回合制java游戏_浅谈游戏的战斗方式分类:回合制,半回合制,即时制...

    游戏的分类很多,按照题材可分为动作.冒险.角色扮演.策略.模拟.音乐.休闲.体育等.今天聊一下传统的战斗方式分类: 回合制,半回合制,即时制. 1.回合制RPG:代表作品:仙剑奇侠传.轩辕剑等. 优点 ...

  7. 浅谈Flutter跨平台调用方式MethodChannel

    Flutter是目前非常流行的跨平台方案,由于它的性能接近于原生应用,因而被越来越多的开发者所采用.既然是跨平台方案,那么久必然存在调用系统功能的需求,在Flutter中,Flutter层与nativ ...

  8. 浅谈Qt的编译方式:qmake/cmake/qbs及qbs被弃用的原因

    1.Qbs简介 https://doc.qt.io/qbs/qml-qbsmodules-qbs.html Qt 编译方式之 qbs_lucky-billy的博客-CSDN博客_qbs QBS(Qt ...

  9. 小浩浅谈 程序/进程/线程,并行/并发

    1.程序,随着时代和社会的发展,我们开始学习各种各样的计算机语言,开始离不开编程,但是我们需要了解的是,什么是程序?程序代表什么?以及程序的作用?以及我们编程,编写程序是为了干嘛? 在这,我认为程序指 ...

最新文章

  1. 17 个品牌,113 款 5G 手机,5G 离我们越来越近
  2. Leetcode Excel Sheet Column Number
  3. [转载]常用正则表达式
  4. matlab产生时间数组以月为单位_Matlab中处理日期与时间的函数
  5. 重磅 | 2017年全球最聪明公司排行榜TOP50,哪9家中国公司上榜?
  6. c语言偏移一个字节,文件偏移量与C语言中的流定位
  7. DotNetty 实现 Modbus TCP 系列 (三) Codecs Handler
  8. 清除Linux终端命令的历史记录
  9. 凉山州计算机等级考试时间,2020年四川凉山中考考试时间及科目安排(已公布)...
  10. 小米球穿透工具使用步骤
  11. 切向量,法向量,梯度
  12. 云运维拓扑图_云平台网络拓扑图
  13. 12月编程语言排行榜出炉
  14. dvwa上传php文件,DVWA之文件上传漏洞
  15. 用python做贪吃蛇
  16. C#工业触摸屏上位机源码 纯源代码 替代传统plc搭载的触摸屏
  17. 爬取王者荣耀官网英雄皮肤高清壁纸(简易方法)
  18. 移动端的ajax请求(痛的领悟)
  19. 智慧城市的发展过程中,最需要的15个工作岗位
  20. Android socket 实现 wify 通信,简易聊天室 (一)

热门文章

  1. lisp用entmake生产圆柱体_CAD与Excel之间数据传递的思路 1
  2. 年薪百万程序员的成功基础
  3. 获取android Application的方法
  4. 成为Java顶尖程序员 ,看这10本书就够了
  5. Python——滑雪游戏
  6. 商家中心设计 多sku商品发布_拼多多商品及sku发布规则
  7. 极速上手 VUE 3——teleport传送门组件
  8. C语言练习题:作品得分(数组)
  9. 以面试官的角度谈校招简历
  10. docker部署redis集群实现动态扩缩容