GoLang: Defer,Panic,Recover,Closure

GoLang: Defer,Panic,Recover,Closure

本文主要内容来自:

GoLang Blog--Defer, Panic, and Recover

GoLang Spec--Defer statements

接下来先简要的介绍defer,panic和recover的使用,最后在介绍一些defer和closure(闭包)组合在一起的语法上的细节。

defer用于一个函数体之内,它会在包裹着它的函数返回之后再执行。通常来说defer都用于资源的释放,比如说文件的关闭,网络连接的断开,锁的释放等。有时候函数变得很长,可能到后面程序员自己都忘了关闭这些资源,那么会导致内核的资源枯竭了,比如说没有可用的文件描述符,这就是defer出现的原因。

代码如下:

src, err := os.Open(srcName)
defer src.Close()

Defer

defer很好理解。对于defer的行为来说,主要有以下三条规则:

  1. A deferred function's arguments are evaluated when the defer statement is evaluated.
  2. Deferred function calls are executed in Last In First Out order after the surrounding function returns.
  3. Deferred functions may read and assign to the returning function's named return values.

接下来对三条内容分别解释。
A deferred function's arguments are evaluated when the defer statement is evaluated.

这里说的defer 修饰的函数,它的参数在编译期间(更好的说法应该是执行期间)就决定了,而不是在运行时(更好的说法应该是调用期间)才决定的(个人理解,认为这样的表述更好懂).

比如说以下代码:

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

虽然后面的代码对i做了修改,但是并不会影响到defer的结果。因为在编译期间就决定了.再来看另外一个代码来理解:

func trace(i int) int {
    fmt.printf("%d \n",i)
    return i
}

func foo() {
    defer func(i int) {
        fmt.printf("defer:%d \n",i)
    }(trace(1))
    fmt.println("hello world")
}
func main() {
    foo()
}
//output:
// 1
// hello world
// defer:1

从代码的输出来说,函数的执行顺序为:trace() -> foo() -> defer
我们看到,虽然defer没有执行,但是它的参数已经初始化好了,由trace()函数返回。这也就是Golang Spec-defer中所说的:

Each time a "defer" statement executes, the function value and parameters to the call are evaluated as usual and saved anew but the actual function is not invoked。

Deferred function calls are executed in Last In First Out order after the surrounding function returns.

如果在一个函数内部,有多个defer修饰的语句。那么他们的执行顺序是以后进先出的,和栈比较类似。我想在汇编层面也应该是将defer压入到栈中,然后再从栈当中弹出的。

下面代码的输出结果是:3,2,1,0.这和他们defer的顺序相反.

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

Deferred functions may read and assign to the returning function's named return values.

这一点比较难懂,它说deferred function读取或者修改要返回的函数的返回值。首先先介绍什么是named return values。

普通的函数来说,如果有返回值我们只要在函数前面之后指定返回值类型就行。示例代码如下:

//named return 有多种写法
//第一种,直接返回
func foo(i int )(res int) {
    var num = i + 1
    return num
}

//也可以不指定返回值的名字,代码如下
func foo(i int) (res int) {
    res = i + 1
    return 
}

两种写法都是可以的,不过感觉还是第一种写法更好,看起来直观一些.

接下来在和defer组合在一起,我们使用defer来修改named return value的值.代码如下:

func foo() (i int) {
    defer func() {
      i = 200  
    }()
    return 1
}
func main() {
    println(foo())
}
//output: 200

可以看到,返回值被defer函数修改了。然而,如果不是named return,这种情况并不会出现,并不会修改返回值。代码如下:

func foo() int {
    var i = 100
    defer func() {
        i = 200
        println(i)
    } ()
    return i
}
//output: 200 100

之所以会出现这种情况,是因为在他们的汇编层面代码中是大不一样的。不过go用的是plan 9,有时间再学吧,只会x86看不太懂。第三条特性使得我们在处理异常的时候,可以很方便的修改错误的返回值。

Panic

在c语言编程中,panic能够让当前程序停下来,以为发生错误了。在golang当中,也是与之类似,panic和defer以及recover组合在一起有非同寻常的能力。文档中提到Golang Spec--panic:

While executing a function F, an explicit call to panic or a run-time panic terminates the execution of F. Any functions deferred by F are then executed as usual. Next, any deferred functions run by F's caller are run, and so on up to any deferred by the top-level function in the executing goroutine. At that point, the program is terminated and the error condition is reported, including the value of the argument to panic. This termination sequence is called panicking..

上面没有明显提到,panic()调用defer之后会返回到caller。blog和panic源码中都说明了defer之后会回到caller

Panic is a built-in function that stops the ordinary flow of control and begins panicking. When the function F calls panic, execution of F stops, any deferred functions in F are executed normally, and then F returns to its caller. To the caller, F then behaves like a call to panic. The process continues up the stack until all functions in the current goroutine have returned, at which point the program crashes. Panics can be initiated by invoking panic directly. They can also be caused by runtime errors, such as out-of-bounds array accesses.

panic能够结束当前代码的执行,然后跳转到当前函数的defer中去执行,defer函数执行完了之后就返回到函数的调用者。对于调用者来说,它就可以认为调用F发生了panic。然后就将调用者的defer也逐个去执行,然后整个程序就会停止运行。panic可以由显式的调用panic或者运行时错误产生。

先给一段示例代码来感受一下defer和panic的过程,代码如下:

func foo() {
    defer func() {
        fmt.println("after panic")
    } ()
    fmt.println("foo")
    panic("fatal error")
}
func main() {
    defer func() {
        pritln("main panic")
    }
    foo3()
    fmt.println("after foo3")
}
//output
foo 
after panic
main panic
panic:fatal error

上结果中可以观察到:

  1. 发生panic的函数defer执行在caller的defer之前
  2. 等待所有的defer函数执行完之后,panic()之后的语句不会被运行
  3. caller和发生panic的函数内部的defer顺序是FIFO的,和第二条规则中有些不一样.

Recover

golang中对于异常处理没有像其他语言那样try catch直观。为了从panic中恢复,继续执行代码。golang提供了recover函数来从panic中恢复,继续执行代码。

引用文档中的描述:

The recover function allows a program to manage behavior of a panicking goroutine. Suppose a function G defers a function D that calls recover and a panic occurs in a function on the same goroutine in which G is executing. When the running of deferred functions reaches D, the return value of D's call to recover will be the value passed to the call of panic. If D returns normally, without starting a new panic, the panicking sequence stops. In that case, the state of functions called between G and the call to panic is discarded, and normal execution resumes. Any functions deferred by G before D are then run and G's execution terminates by returning to its caller

也就是说,假设函数g defer了函数d,d中调用recover,在g当中发生了panic。那么就函数d中的recover()的返回值就是引起panic的err。如果d可以正常执行,在d执行的过程中没有产生新的panic,那么panic就会被停止,而且代码会继续运行下去。这里说的继续运行不是说沿着刚才发生panic的地方继续下去,而是不会让这个panic终止整个程序运行。

下面先是用一段简单的数组越界来作为异常的例子:

func foo() (res int) {
    arr := make([]int,2)
    var num = arr[3]
    return num
}
func main() {
    foo()
    fmt.println("main") //会因为数组越界而无法进行运行
}

这段代码会因为数组越界而直接无法运行。结合recover我们可以来捕捉到这个情况,并且恢复程序的继续运行.

接下来,我们来演示使用recover,panic,defer一起来处理代码中的异常,如下:

//会引发数组越界
func foo() (res int) {
    defer func() { //panic之后defer会返回到调用函数
        if err := recover(); err != nil {
            fmt.println("Recover")
            res = -1 //修改返回值
       }
    }()
    arr := make([]int,2)
    var num = arr[3]
    return num
}
func main() {
    print(foo())
    fmt.println("main")
}
//output:
-1 
main

可以看到,在defer中对数组越界做了处理返回到了主函数中,主函数可以继续执行。我们通过named return 和defer结合在一起修改了返回值。类似java和一些其他语言中的try catch.

此外,在这段代码中,defer的代码将返回值返回了,但是defer的内部没有return,这看起来十分奇怪。这是因为panic之后调用defer最终会回到调用函数中,而在named return 中,可以对返回值进行修改。

Closure

在很多函数中,都支持以函数作为返回值,作为参数等特性。在这些变成语言中,都有闭包的存在。闭包的定义十分晦涩,维基百科上的定义看不懂。引用一个来子stackoverflow的回答,what is closure?

A closure is a persistent local variable scope.

A closure is a persistent scope which holds on to local variables even after the code execution has moved out of that block. Languages which support closure (such as JavaScript, Swift, and Ruby) will allow you to keep a reference to a scope (including its parent scopes), even after the block in which those variables were declared has finished executing, provided you keep a reference to that block or function somewhere

闭包可以让局部变量也可以被持久地访问,即使函数运行结束了。而且,像在c语言中,在函数体内部定义的局部变量不能让函数外部来访问,然而闭包却可以实现这一点

一段简短的闭包代码:

func foo() func() int { //声明返回一个func() int类型的函数
    var a = 1
    return func() int {
        return a
    }
}
func main() {
    function := func()
    var num = function()
    print(num)
}

照理来说,foo()中的a在运行完了就应该被释放了,但是在闭包当中仍然可以使用。

从另外一个角度来说,所返回的函数像是一个对象,对于闭包内的变量更新会被保存下来。看如下代码:

func foo() func() int {
    var a = 1
    return func() int {
        a++
        return a
    }
}
func main() {
    var function = foo()
    println(function())
    println(function())
}
//output: 2 3

可以看到上次所运行的结果被保存到了下一次的运行。

defer 和 closure:

func foo() int {
    var a = 1
    defer func() {
        a++
        fmt.printf("defer:%d \n",a)
    }()
    a = 3 //是闭包,会对defer产生影响
    return a
}
//output: 4

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

在函数foo()中,构成了闭包,和下面a()中的情况不同。defer会将函数延迟到包裹着它的主函数return之后再去执行(并不完全对),因此呢对于局部变量a的修改会直接影响到闭包内的变量a。

前面我们说,如果defer修饰的函数的参数在编译期间就确定了,但是并没有被调用(不是特别准确的说法)。将foo()修改为:

func foo() int {
    var a = 1
    defer func(i int ){
       i++
       fmt.printf("defer:%d ",i)
    }(a)
    a = 3
    return a
}
//output: 2

作为参数的时候,后续对a的修改不会影响到defer的函数。可以认为(我是这么认为),闭包内的变量的值是在调用期间才确认的,而不是在执行期间,我没有找到官方文档中具体的对于闭包的具体描述。

再来看一个例子:

var whatever [5]struct{}
for i := range whatever {
    fmt.println(i)
}
//output:0,1,2,3,4

for i := range whatever {
    defer func() {
        fmt.println(i)
    }
}
//output: 4,4,4,4,4

for i := range whatever {
    defer func(n int ) {
        fmt.Println(n)
    }(i)
}
//output: 4,3,2,1,0

第一个例子只是最简单的循环,输出0-4没什么好说的。第二个是闭包,闭包内的变量是在调用的时候才确定的。当defer开始调用的时候,for循环早就结束了,此时i=4.最后一个就是defer的参数在执行期间就决定了,所以会输出 4,3,2,1,0. 注意下面说的execute和invoke的区别.

Each time a "defer" statement executes, the function value and parameters to the call are evaluated as usual and saved anew but the actual function is not invoked.

Why

为什么defer和named return组合在一起能够修改返回值呢?我试图深入到汇编层面来理解,不过不懂plan 9的语法,看不懂。只能草草略过。

示例代码:

func foo() (res int) {
    defer func() {
        res = 4321
    }()
    return 1234
}
func bar() int {
    return 100
}

func main() {
    println(foo())
    println(bar())
}
//output:4321 100

为了从汇编层面来解释这个情况,先提前说明defer执行的时间到底是在什么时候。

golang spec --golang中说了defer实际的执行时间是:返回值被装配好之后,但是实际返回到调用者之前

That is, if the surrounding function returns through an explicit return statement, deferred functions are executed after any result parameters are set by that return statement but before the function returns to its caller

我们使用如下语句将foo和bar的汇编指令都输出:

go build -gcflags '-l' -o main main.go
go tool objdump -s "main\.bar" main > bar.txt
go tool objdump -s "main\.foo" main > foo.txt

首先来看bar的汇编代码:

TEXT main.bar(SB) /home/ygj/Desktop/gofoo/main.go
  main.go:11        0x45dce0        48c744240864000000  MOVQ $0x64, 0x8(SP) 
  main.go:11        0x45dce9        c3          RET         

可以看到return语句其实是分为:将返回值放到某个位置(看起来是栈),然后ret指令返回。defer语句就是执行在这两句话中间的,下面是foo的汇编代码,很多看不懂:

....略       
  main.go:7     0x45dc8e        48c7442430d2040000  MOVQ $0x4d2, 0x30(SP) <--- 原来的返回值1234            
  main.go:7     0x45dc97        c644240f00      MOVB $0x0, 0xf(SP)          
  main.go:7     0x45dc9c        488b442410      MOVQ 0x10(SP), AX           
  main.go:7     0x45dca1        48890424        MOVQ AX, 0(SP)              
  main.go:7     0x45dca5        e8f6000000      CALL main.foo.func1(SB)     <----调用defer func()  
  main.go:7     0x45dcaa        488b6c2420      MOVQ 0x20(SP), BP           
  main.go:7     0x45dcaf        4883c428        ADDQ $0x28, SP              
  main.go:7     0x45dcb3        c3          RET                 
  main.go:7     0x45dcb4        e887e3fcff      CALL runtime.deferreturn(SB)        
  main.go:7     0x45dcb9        488b6c2420      MOVQ 0x20(SP), BP           
  main.go:7     0x45dcbe        4883c428        ADDQ $0x28, SP              
  main.go:7     0x45dcc2        c3          RET                 
  main.go:3     0x45dcc3        e8b8aeffff      CALL runtime.morestack_noctxt(SB)   
  main.go:3     0x45dcc8        e973ffffff      JMP main.foo(SB)            

TEXT main.foo.func1(SB) /home/ygj/Desktop/gofoo/main.go
  main.go:5     0x45dda0        488b442408      MOVQ 0x8(SP), AX    
  main.go:5     0x45dda5        48c700e1100000      MOVQ $0x10e1, 0(AX) <-------返回4321
  main.go:6     0x45ddac        c3          RET 

但是1234对应的16进制是0x4d2,4321对应的十六进制是0x10e1。草草看下汇编代码,其实可以发现在赋值1234和返回4321之间执行了defer的函数。所以呢也自然可以对返回值做修改再返回。

summary

本文呢粗略的对defer,panic,recover做了一个简要的减少,帮助理解这些函数的基本使用。对于defer的行为可以进一步的深入到go的源码中查看具体的行为是怎么样的。这部分比较复杂,以后水平高了再补。

Related

Behavior of defer function in named return function这篇粗略了介绍了汇编层面的defer情况,但是没有讲述汇编代码的具体意思

How do JavaScript closures work at a low level? 介绍了javascript中闭包是如何实现的,我没有看过有点长,不过应该有启发性的作用。

closures演示了闭包的基本用法

暂无评论

发送评论 编辑评论

|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇