前言

我们知道Golang处理异常是用error返回的方式,然后调用方根据error的值走不同的处理逻辑。但是,如果程序触发其他的严重异常,比如说数组越界,程序就要直接崩溃。Golang有没有一种异常捕获和恢复机制呢?这个就是本文要讲的panic和recover。其中recover要配合defer使用才能发挥出效果。

Defer

Defer语句将一个函数放入一个列表(用栈表示其实更准确)中,该列表的函数在环绕defer的函数返回时会被执行。defer通常用于简化函数的各种各样清理动作,例如关闭文件,解锁等等的释放资源的动作。例如下面的这个函数打开两个文件,从一个文件拷贝内容到另外的一个文件:

func CopyFile(dstName, srcName string) (written int64, err error) {
src, err := os.Open(srcName)
if err != nil {
return
} dst, err := os.Create(dstName)
if err != nil {
return
} written, err = io.Copy(dst, src)
dst.Close()
src.Close()
return
}

这段代码可以工作,但是有一个bug。如果调用os.Create失败,函数将会直接返回,并没有关闭srcName文件。修复的方法很简单,可以把src.Close的调用放在第二个return语句前面。但是当我们程序的分支比较多的时候,也就是说当该函数还有几个其他的return语句时,就需要在每个分支return前都要加上close动作。这样使得资源的清理非常繁琐而且容易遗漏。所以Golang引入了defer语句:

func CopyFile(dstName, srcName string) (written int64, err error) {
src, err := os.Open(srcName)
if err != nil {
return
}
defer src.Close() dst, err := os.Create(dstName)
if err != nil {
return
}
defer dst.Close() return io.Copy(dst, src)
}

在每个资源申请成功的后面都加上defer自动清理,不管该函数都多少个return,资源都会被正确的释放,例如上述例子的文件一定会被关闭。

关闭defer语句,有三条简单的规则:

1.defer的函数在压栈的时候也会保存参数的值,并非在执行时取值。

func a() {
i :=
defer fmt.Println(i)
i++
return
}

例如该示例中,变量i会在defer时就被保存起来,所以defer函数执行时i的值是0.即便后面i的值变为了1,也不会影响之前的拷贝。

2.defer函数调用的顺序是后进先出。

func b() {
for i := ; i < ; i++ {
defer fmt.Print(i)
}
}

函数输出3210

3.defer函数可以读取和重新赋值函数的命名返回参数。

func c() (i int) {
defer func() { i++ }()
return
}

这个例子中,defer函数中在函数返回时对命名返回值i进行了加1操作,因此函数返回值是2.可能你会有疑问,规则1不是说会在defer时保存i的值吗?保存的i是0,那加1操作之后也是1啊。这里就是闭包的魅力,i的值会被立马保存,但是保存的是i的引用,也可以理解为指针。当实际执行加1操作时,i的值其实被return置为了1,defer执行了加1操作i的值也就变成了2.

Panic

Panic是内建的停止控制流的函数。相当于其他编程语言的抛异常操作。当函数F调用了panic,F的执行会被停止,在F中panic前面定义的defer操作都会被执行,然后F函数返回。对于调用者来说,调用F的行为就像调用panic(如果F函数内部没有把panic recover掉)。如果都没有捕获该panic,相当于一层层panic,程序将会crash。panic可以直接调用,也可以是程序运行时错误导致,例如数组越界。

Recover

Recover是一个从panic恢复的内建函数。Recover只有在defer的函数里面才能发挥真正的作用。如果是正常的情况(没有发生panic),调用recover将会返回nil并且没有任何影响。如果当前的goroutine panic了,recover的调用将会捕获到panic的值,并且恢复正常执行。

例如下面这个例子:

package main

import "fmt"

func main() {
f()
fmt.Println("Returned normally from f.")
} func f() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered in f", r)
}
}()
fmt.Println("Calling g.")
g()
fmt.Println("Returned normally from g.")
} func g(i int) {
if i > {
fmt.Println("Panicking!")
panic(fmt.Sprintf("%v", i))
}
defer fmt.Println("Defer in g", i)
fmt.Println("Printing in g", i)
g(i + )
}

函数g接受参数i,如果i大于3时触发panic,否则对i进行加1操作。函数f的defer函数里面调用了recover并且打印recover的值(非nil的话)。

程序将会输出:

Calling g.
Printing in g
Printing in g
Printing in g
Printing in g
Panicking!
Defer in g
Defer in g
Defer in g
Defer in g
Recovered in f
Returned normally from f.

Panic和recover可以接受任何类型的值,因为定义为interface{}:

func panic(v interface{})

func recover() interface{}

所以工作模式相当于:

panic(value)->recover()->value

传递给panic的value最终由recover捕获。

另外defer可以配合锁的使用来确保锁的释放,例如:

mu.Lock()

Defer mu.Unlock()

需要注意的是这样会延长锁的释放时间(需要等到函数return)。

容易踩坑的一些例子

通过上面的说明,我们已经对defer,panic和recover有了比较清晰的认识,下面通过一些实战中容易踩坑的例子来加深下印象。

在循环里面使用defer

不要在循环里面使用defer,除非你真的确定defer的工作流程,例如:

只有当函数返回时defer的函数才会被执行,如果在for循环里面defer定义的函数会不断的压栈,可能会爆栈而导致程序异常。

解决方法1:将defer移动到循环之外

解决方法2:构造一层新的函数包裹defer

defer方法

没有指针的情况:

type Car struct {
model string
}
func (c Car) PrintModel() {
fmt.Println(c.model)
}
func main() {
c := Car{model: "DeLorean DMC-12"}
defer c.PrintModel()
c.model = "Chevrolet Impala"
}

程序输出DeLorean DMC-12。根据我们前面讲的内容,defer的时候会把函数和参考拷贝一份保存起来,所以c.model的值后面改变也不会影响defer的运行。

有指针的情况:

Car PrintModel()方法定义改为:

func (c *Car) PrintModel() {
fmt.Println(c.model)
}

程序将会输出Chevrolet Impala。这些defer虽然将函数和参数保存了起来,但是由于参数的值本身是针对,随意后面的改动会影响到defer函数的行为。

同理的例子还有:

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

程序将会输出:


因为闭包引用匿名函数外面的变量相当于是指针引用,得到的是变量的地址,实际到defer真正执行时,指针指向的内容已经发生的变化:

解决的方法:

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

或者:

for i := ; i < ; i++ {
defer fmt.Println(i)
}

程序输出:


这里就不会用到闭包的上下文引用特性,是正经的函数参数拷贝传递,所以不会有问题。

defer中修改函数error返回值

package main

import (
"errors"
"fmt"
) func main() {
{
err := release()
fmt.Println(err)
} {
err := correctRelease()
fmt.Println(err)
}
} func release() error {
defer func() error {
return errors.New("error")
}() return nil
} func correctRelease() (err error) {
defer func() {
err = errors.New("error")
}()
return nil
}

release函数中error的值并不会被defer的return返回,因为匿名返回值在defer执行前就已经声明好并复制为nil。correctRelease函数能够修改返回值是因为闭包的特性,defer中的err是实际的返回值err地址引用,指向的是同一个变量。defer修改程序返回值error一般用在和recover搭配中,上述的情况属于滥用defer的一种情况,其实error函数值可以直接在程序的return中修改,不用defer。

总结

文章介绍了defer、panic和recover的原理和用法,并且在最后给出了一些在实际应用的实践建议,不要滥用defer,注意defer搭配闭包时的一些特性。

参考

https://blog.golang.org/defer-panic-and-recover

https://blog.learngoprogramming.com/gotchas-of-defer-in-go-1-8d070894cb01

https://blog.learngoprogramming.com/5-gotchas-of-defer-in-go-golang-part-ii-cc550f6ad9aa

https://blog.learngoprogramming.com/golang-defer-simplified-77d3b2b817ff

https://blog.learngoprogramming.com/5-gotchas-of-defer-in-go-golang-part-iii-36a1ab3d6ef1


Golang 高效实践之defer、panic、recover实践的更多相关文章

  1. Go语言异常处理defer\panic\recover

    Go语言追求简洁优雅,所以,Go语言不支持传统的 try…catch…finally 这种异常,因为Go语言的设计者们认为,将异常与控制结构混在一起会很容易使得代码变得混乱.因为开发者很容易滥用异常, ...

  2. defer,panic,recover

    Go语言不支持传统的 try…catch…finally 这种异常,因为Go语言的设计者们认为,将异常与控制结构混在一起会很容易使得代码变得混乱.因为开发者很容易滥用异常,甚至一个小小的错误都抛出一个 ...

  3. defer, panic, recover使用总结

    1. defer : 延迟调用.多个defer,依次入栈,在函数即将退出时,依次出栈调用 package main import "fmt" func main() { defer ...

  4. Go的异常处理 defer, panic, recover

    Go语言追求简洁优雅,所以,Go语言不支持传统的 try…catch…finally 这种异常,因为Go语言的设计者们认为,将异常与控制结构混在一起会很容易使得代码变得混乱.因为开发者很容易滥用异常, ...

  5. go语言defer panic recover用法总结

    defer defer是go提供的一种资源处理的方式.defer的用法遵循3个原则 在defer表达式被运算的同时,defer函数的参数也会被运算.如下defer的表达式println运算的同时,其入 ...

  6. Golang 入门系列(十四)defer, panic和recover用法

    以前讲过golang 的基本语法.但是,只是讲了一些基础的语法,感兴趣的可以看看以前的文章,https://www.cnblogs.com/zhangweizhong/category/1275863 ...

  7. go panic recover 异常处理

    Go语言追求简洁优雅,所以,Go语言不支持传统的 try…catch…finally 这种异常,因为Go语言的设计者们认为,将异常与控制结构混在一起会很容易使得代码变得混乱.因为开发者很容易滥用异常, ...

  8. Golang高效实践之泛谈篇

    前言 我博客之前的Golang高效实践系列博客中已经系统的介绍了Golang的一些高效实践建议,例如: <Golang高效实践之interface.reflection.json实践>&l ...

  9. Golang 高效实践之并发实践context篇

    前言 在上篇Golang高效实践之并发实践channel篇中我给大家介绍了Golang并发模型,详细的介绍了channel的用法,和用select管理channel.比如说我们可以用channel来控 ...

随机推荐

  1. 分布式文件系统之MooseFS----介绍

    前一段公司因为nfs存储的问题,做了一段时间的调研.最终虽然有nfs高可用方案(nfs+drbd+heartbeat),但是在nfs故障切换的时候会出现 2 分钟左右的延时.这样子...就开始了对分布 ...

  2. web.congfig 禁用 ViewState Session

    <!--禁用 ViewState Session--> <pages enableViewState="false" enableSessionState=&qu ...

  3. Mariadb的安装与使用

    一.安装Mariadb 参考博客:https://www.cnblogs.com/pyyu/p/9467289.html 安装软件的三中方式 yum原码编译安装下载rpm安装 yum与原码编译安装安装 ...

  4. 使用 Cake 推送 NuGet 包到 AzureDevops 的 Artifacts 上

    前言 大家好,我最近在想如何提交代码的时候自动的打包 NuGet 然后发布到 AzureDevOps 中的 Artifacts,在这个过程中踩了很多坑,也走了很多弯路,所以这次篇文章就是将我探索的结果 ...

  5. 5个现在就该使用的数组Array方法: indexOf/filter/forEach/map/reduce详解(转)

    ECMAScript5标准发布于2009年12月3日,它带来了一些新的,改善现有的Array数组操作的方法.然而,这些新奇的数组方法并没有真正流行起来的,因为当时市场上缺乏支持ES5的浏览器.     ...

  6. python-基本数据类型(int,bool,str)

    一.python基本数据类型 1. int ==>  整数. 主要⽤用来进⾏行行数学运算 2. str ==> 字符串串, 可以保存少量量数据并进⾏行行相应的操作 3. bool==> ...

  7. Fabric1.4源码解析:Peer节点背书提案过程

    以前从来没有写过博客,从这段时间开始才开始写一些自己的博客,之前总觉得写一篇博客要耗费大量的时间,而且写的还是自己已经学会的,觉得没什么必要.但是当开始用博客记录下来的时候,才发现有些学会的地方只是自 ...

  8. 宜信开源|手把手教你安装第一个LAIN应用

    LAIN是宜信公司大数据创新中心开发的开源PaaS平台.在金融的场景下,LAIN 是为解放各个团队和业务线的生产力而设计的一个云平台.LAIN 为宜信大数据创新中心各个团队提供了统一的测试和生产环境, ...

  9. node实现文件拷贝1

    nodeJS对文件的复制: 一般对于小型文件的复制操作使用的是流的管道运输操作, 首先需要加载引入的文件:var fs = require('fs'); 1.同步创建文件夹 fs.mkdirSync( ...

  10. Linux下多网卡绑定bond及模式介绍

    [介绍] 网卡bond一般主要用于网络吞吐量很大,以及对于网络稳定性要求较高的场景. 主要是通过将多个物理网卡绑定到一个逻辑网卡上,实现了本地网卡的冗余,带宽扩容以及负载均衡. Linux下一共有七种 ...