go defer-recover-panic 学习

本文将会讲解defer, recover,panic相关的知识。主要内容包括:

  • defer的原理
  • panic与recover的原理及注意事项

其中重点在defer的原理。这部分包含了defer的定义、规则、实现原理、内部函数顺序四部分。

希望看完本文,你能对defer、recover、panic有个全面的认识~

一、defer的原理

定义

1、defer语句用于延迟函数的调用,每次defer都会把一个函数压入栈中,主函数(创建defer的函数)返回前再把延迟的函数取出并执行。defer最常见的场景是完成一些收尾的工作,比如文件句柄的关闭等。还有就是执行 recover, 实现类似其他语言中的try catch finally。
2、延迟函数可能有输入参数,这些参数可能来源于定义defer的函数,延迟函数也可能引用主函数用于返回的变量,也就是说延迟函数可能会影响主函数的一些行为。

规则

规则一、其实使用defer时,用一个简单的转换规则改写一下,就不会迷糊了。改写规则是将return语句拆成两句写,return xxx会被改写成:
返回值 = xxx
调用defer的函数
空的return
规则一的几个案例

下面通过一些案例进行总结:

题目一
func deferFuncParameter() {    
    var aInt = 1
    defer fmt.Println(aInt)
    aInt = 2
    return
}

延迟函数fmt.Println(aInt) 在defer语句出现时就已经确定了,所以无论后面如何修改aInt的值都不会影响延迟函数。
上述程序转换之后是这样的:

func deferFuncParameter() {    
    var aInt = 1
    anonymous = aInt // anonymous为匿名的变量
    aInt = 2
    fmt.Println(anonymous)
    return
}

即使是结构体,也是传值,也不会影响。比如

type Test struct {
    value int
}

func (t Test) print() {
    println(t.value)
}

func main() {
    test := Test{}
    defer test.print()
    test.value += 1
}

这段代码输出的也是0.
如果是结构体指针,则会影响输出。

type Test struct {
    value int
}

func (t *Test) print() {
    println(t.value)
}

func main() {
    test := Test{}
    defer test.print()
    test.value += 1
}

这个输出的就是1, 因为传递的指针。

题目二
func printArray(array *[3]int) {    
   for i := range array {
       fmt.Println(array[i])
   }
}

func deferFuncParameter() {
   var aArray = [3]int{1, 2, 3}    
   defer printArray(&aArray)
   aArray[0] = 10
   return
}

func main() {
   deferFuncParameter()
}

函数deferFuncParameter定义了一个数组,通过defer调用printArray, 最后修改数组的第一个元素。printArray 函数接收数组的指针,即数组的地址,由于延迟函数执行时机在return语句之前,所以对数组的最终修改值被打印出来。

题目三
func deferFuncReturn() (result int) {    
    i := 1
    defer func() {
       result++
    }()    
    return i
}

函数的return语句并不是原子的,实际执行分为设置返回值->ret。defer语句实际执行在主函数返回(ret)前,即拥有defer的函数返回过程是 : 设置返回值->执行defer->ret。所以return语句先把result设置为i的值,即1,defer语句中又把result递增1,所以最终返回的是2.
上述程序可以转换为

func deferFuncReturn() (result int) {    
    i := 1
    result = i
    func() {
       result++
    }()    
    return 
}

总结上文的例子可以得出如下几个结论:

  • 延迟函数的参数在defer语句出现时就已经确定下来了
    如果是字面量,则肯定不受影响(如题目一); 如果是指针类型,规则仍然适用,只不过延迟函数的参数是一个地址值,这种情况下defer后面的语句对变量的修改可能会影响延迟函数(如题目二)。
  • 延迟函数可能操作主函数的具名返回值
    关键字return不是一个原子操作,实际上return只代理汇编指令ret,即将跳转程序执行。比如语句return i,实际上分两步进行,即将i值存入栈中作为返回值,然后执行跳转,而defer的执行时机正是跳转前,所以说defer执行时还是有机会操作返回值的。
规则二、从主函数返回值的角度看,有如下的几条规则:
1、主函数拥有匿名返回值,返回字面值
func foo() int {    
    var i int
    defer func() {
        i++
    }()    
    return 1
}

一个主函数拥有一个匿名的返回值,返回时使用字面值,比如”1“, ”hello“这样的值,这种情况下defer是无法操作返回值的。

2、主函数拥有匿名返回值,返回变量

一个主函数拥有一个匿名的返回值,返回使用本地或全局变量,这种情况下defer语句可以引用到返回值,但不会改变返回值。

func foo() int {    
    var i int
    defer func() {
        i++
    }()    
    return i
}

上面的函数,返回一个局部变量,同时defer函数也会操作这个局部变量。对于匿名返回值来说,可以假定仍然有一个变量存储返回值,假定返回值变量为"anony",上面的返回语句可以拆分成以下过程:

anony = i
i++
return

由于i是整型,会将值拷贝给anony,所以defer语句修改i值,对函数返回值不会造成影响。

3、主函数拥有具名返回值

主函数声明语句中带有名字的返回值,会被初始化一个局部变量,函数内部可以像使用局部变量一样使用该返回值。如果defer语句操作该返回值,可能会改变返回结果。

func foo() (ret int) {    
    defer func() {
        ret++
    }()    
    return 0}

上面的函数拆解之后是这样的:

ret = 0
ret++
return

defer实现原理

数据结构

每个goroutine数据结构中实际上也有一个defer指针,该指针指向一个defer的单链表,每次声明一个defer时就将defer插入到单链表表头,每次执行defer时就从单链表表头取出一个defer执行。


image.png
defer的创建和执行

源码包src/runtime/panic.go定义了两个方法分别用于创建defer和执行defer。

  • deferproc(): 在声明defer处调用,其将defer函数存入goroutine的链表中;
  • deferreturn():在return指令,准确的讲是在ret指令前调用,其将defer从goroutine链表中取出并执行。

可以简单这么理解,在编译在阶段,声明defer处插入了函数deferproc(),在函数return前插入了函数deferreturn()。

defer内部函数顺序

func TestDefer(t *testing.T) {
    fmt.Println("a")
    defer fmt.Println("b")
    defer c()
    defer d()
    fmt.Println("f")
}

func c() {
    fmt.Println("c")
}

func d() func(){
    fmt.Println("d")
    return func() {
        fmt.Println("e")
    }
}

输出为 a f d c b
结论:defer函数在函数执行结束后执行,若有多个defer函数,则执行顺序为后进先出。 主函数中,defer d() 并没有执行d()返回的闭包,所以结果里面并没有返回e.

func TestDefer(t *testing.T) {
    fmt.Println("a")
    defer fmt.Println("b")
    defer c()
    defer d()()
    fmt.Println("f")
}

func c() {
    fmt.Println("c")
}

func d() func(){
    fmt.Println("d")
    return func() {
        fmt.Println("e")
    }
}

这段代码只是在调用d方法时加了个括号,那么d方法返回的方法就会立即执行
返回结果为 a d f e c b 。 为什么不是 a f d e c b 呢? 这是因为在defer d()() 编译时,首先定义了函数d(), 此时就输出了d. 然后返回包含e的闭包函数。
即被defer标记的d函数中的程序“立即执行”,而d函数返回的函数则在测试方法结束后 按照“后进先出”的顺序执行。

再看一个 来自effective go的例子:

func trace(s string) string {
    fmt.Println("entering:", s)
    return s
}

func un(s string) {
    fmt.Println("leaving:", s)
}

func a() {
    defer un(trace("a"))
    fmt.Println("in a")
}

func b() {
    defer un(trace("b"))
    fmt.Println("in b")
    a()
}

func main() {
    b()
}

结果会打印

entering: b
in b
entering: a
in a
leaving: a
leaving: b

没执行以前我以为是如下的结果:

in b
in a
entring: a
leaving: a
entring: b
leaving: b

为什么不对呢?
因为在编译时,b() 中的 defer un(trace("b")) ,是对un()函数的延迟,但是此时会执行trace("b")。
这个例子说明,defer标记的函数只是最外层的函数,如果defer标记函数的参数也是个函数,则作为参数的函数在编译时就会被执行了,不必等到defer标记函数执行时才执行。

二、panic与recover的原理及注意事项

  • panic内置函数停止当前goroutine的正常执行,当函数F调用panic时,函数F的正常执行被立即停止,然后运行所有在F函数中的defer函数,然后F返回到调用他的函数对于调用者G,F函数的行为就像panic一样,终止G的执行并运行G中所defer函数,此过程会一直继续执行到goroutine所有的函数。panic可以通过内置的recover来捕获。
  • recover内置函数用来管理含有panic行为的goroutine,recover运行在defer函数中,获取panic抛出的错误值,并将程序恢复成正常执行的状态。如果在defer函数之外调用recover,那么recover不会停止并且捕获panic错误如果goroutine中没有panic或者捕获的panic的值为nil,recover的返回值也是nil。由此可见,recover的返回值表示当前goroutine是否有panic行为

几个注意的问题

1、defer 表达式的函数如果定义在 panic 后面,该函数在 panic 后就无法被执行到
func main() {
    panic("a")
    defer func() {
        fmt.Println("b")
    }()
}

结果 b没有打印出来
而在defer后panic

func main() {
    defer func() {
        fmt.Println("b")
    }()
    panic("a")
}

结果b被正常打印。

2、F中出现panic时,F函数会立刻终止,不会执行F函数内panic后面的内容,但不会立刻return,而是调用F的defer,如果F的defer中有recover捕获,则F在执行完defer后正常返回,调用函数F的函数G继续正常执行
func G() {
    defer func() {
        fmt.Println("c")
    }()
    F()
    fmt.Println("继续执行")
}

func F() {
    defer func() {
        if err := recover(); err != nil {
            fmt.Println("捕获异常:", err)
        }
        fmt.Println("b")
    }()
    panic("a")
}

结果

捕获异常: a
b
继续执行
c
3、如果F的defer中无recover捕获,则将panic抛到G中,G函数会立刻终止,不会执行G函数内后面的内容,但不会立刻return,而调用G的defer...以此类推
func G() {
    defer func() {
        if err := recover(); err != nil {
            fmt.Println("捕获异常:", err)
        }
        fmt.Println("c")
    }()
    F()
    fmt.Println("继续执行")
}

func F() {
    defer func() {
        fmt.Println("b")
    }()
    panic("a")
}

结果

b
捕获异常: a
c
4、如果一直没有recover,抛出的panic到当前goroutine最上层函数时,程序直接异常终止
func G() {
    defer func() {
        fmt.Println("c")
    }()
    F()
    fmt.Println("继续执行")
}

func F() {
    defer func() {
        fmt.Println("b")
    }()
    panic("a")
}

结果

b
c
panic: a

goroutine 1 [running]:
main.F()
    /xxxxx/src/xxx.go:61 +0x55
main.G()
    /xxxxx/src/xxx.go:53 +0x42
exit status 2
5、recover都是在当前的goroutine里进行捕获的,这就是说,对于创建goroutine的外层函数,如果goroutine内部发生panic并且内部没有用recover,外层函数是无法用recover来捕获的,这样会造成程序崩溃
func G() {
    defer func() {
        //goroutine外进行recover
        if err := recover(); err != nil {
            fmt.Println("捕获异常:", err)
        }
        fmt.Println("c")
    }()
    //创建goroutine调用F函数
    go F()
    time.Sleep(time.Second)
}

func F() {
    defer func() {
        fmt.Println("b")
    }()
    //goroutine内部抛出panic
    panic("a")
}

结果:

b
panic: a

goroutine 5 [running]:
main.F()
    /xxxxx/src/xxx.go:67 +0x55
created by main.main
    /xxxxx/src/xxx.go:58 +0x51
exit status 2
6、recover返回的是interface{}类型而不是go中的 error 类型,如果外层函数需要调用err.Error(),会编译错误,也可能会在执行时panic
func main() {
    defer func() {
        if err := recover(); err != nil {
            fmt.Println("捕获异常:", err.Error())
        }
    }()
    panic("a")
}

编译错误,结果

err.Error undefined (type interface {} is interface with no methods)
func main() {
    defer func() {
        if err := recover(); err != nil {
            fmt.Println("捕获异常:", fmt.Errorf("%v", err).Error())
        }
    }()
    panic("a")
}

结果:

捕获异常: a

参考文献

Go defer实现原理剖析
理解 Go 语言 defer 关键字的原理
defer关键字
golang中的defer函数的执行顺序
go defer,panic,recover详解 go 的异常处理
effective_go中文版
谈谈 panic 和 recover 的原理,讲的比较深入

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容