前言

我们知道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. Another maybe monad library for ruby

    欢迎任何形式的转载,但请务必注明出处:http://www.cnblogs.com/liangjingyang 项目地址:https://github.com/liangjingyang/maybe_ ...

  2. 宿主机与虚拟机系统的USB设备切换

    有时候我们需要在虚拟机的操作系统中进行一些USB设备的测试,但默认情况下USB设备是在宿主机系统里面的,那这个时候我们就要进行切换才能够达到目的,具体要怎么操作呢?下面讲解一下:   1. Ctrl+ ...

  3. How to Use the Dynamic Link Library in C++ Linux (C++调用Delphi写的.so文件)

    The Dynamic Link Library (DLL) is stored separately from the target application and shared among dif ...

  4. Unity 入門 - 延遲解析

    本文大纲: 小引 共享的范例代码 使用 Lazy<T> 使用自动工厂 注入自定义工厂 小引 当我们说「解析某个型别/组件」时,意思通常是呼叫某类别的建构函式,以建立其实例(instance ...

  5. C#基础原理拾遗——引用类型的值传递和引用传递

    以前写博客不深动,只搭个架子,像做笔记,没有自己的思考,也没什么人来看.这个毛病得改,就从这一篇开始- 最近准备面试,深感基础之重要,奈何我不是计算机科班出身,基础方面有些捉襟见肘.短期怎么补?做面实 ...

  6. 因为 'PRIMARY' 文件组已满。请删除不需要的文件、删除文件组中的对象、将其他文件添加到文件组或为文件组中的现有文件启用自动增长,以便增加可用磁盘空间

    导致你的问题的,应该有2种可能性: 1.存放你的primary文件组的磁盘,已经满了: use master--你的数据库名称 go --看看你的primary组里的文件 select ds.name ...

  7. 30212Java_数组

    数组 1.综述 数组是相同类型数据的有序集合.数组描述的是相同类型的若干个数据,按照一定的先后次序排列组合而成. 其中,每一个数据称作一个元素,每个元素可以通过一个索引(下标)来访问它们. 数组的三个 ...

  8. 9个WebGL的演示

    1.  WebGL Water This incredible demo is as fluid as you could believe.  Raise and drop the ball into ...

  9. 【JavaScript】彻底明白this在函数中的指向

    一.this,其实可以类比成人 说到this的话,我们在js中主要研究的都是函数中的this,在javascript中,this代表当前行为的执行主体,而context代表的是当前行为执行的的环境(区 ...

  10. Spring Boot:使用Memcached缓存

    综合概述 Memcached是一个自由开源的,高性能,分布式内存对象缓存系统.Memcached基于内存的key-value存储,用来存储小块的任意数据,这些数据可以是数据库调用.API调用或者是页面 ...