GoLang: Defer,Panic,Recover,Closure
本文主要内容来自:
GoLang Blog--Defer, Panic, and Recover
接下来先简要的介绍defer,panic和recover的使用,最后在介绍一些defer和closure(闭包)组合在一起的语法上的细节。
defer用于一个函数体之内,它会在包裹着它的函数返回之后再执行。通常来说defer都用于资源的释放,比如说文件的关闭,网络连接的断开,锁的释放等。有时候函数变得很长,可能到后面程序员自己都忘了关闭这些资源,那么会导致内核的资源枯竭了,比如说没有可用的文件描述符,这就是defer出现的原因。
代码如下:
src, err := os.Open(srcName)
defer src.Close()
Defer
defer很好理解。对于defer的行为来说,主要有以下三条规则:
- A deferred function's arguments are evaluated when the defer statement is evaluated.
- Deferred function calls are executed in Last In First Out order after the surrounding function returns.
- 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 topanic
or a run-time panic terminates the execution ofF
. Any functions deferred byF
are then executed as usual. Next, any deferred functions run byF'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 topanic
. 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
上结果中可以观察到:
- 发生panic的函数defer执行在caller的defer之前
- 等待所有的defer函数执行完之后,panic()之后的语句不会被运行
- 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 functionG
defers a functionD
that callsrecover
and a panic occurs in a function on the same goroutine in whichG
is executing. When the running of deferred functions reachesD
, the return value ofD
's call torecover
will be the value passed to the call ofpanic
. IfD
returns normally, without starting a newpanic
, the panicking sequence stops. In that case, the state of functions called betweenG
and the call topanic
is discarded, and normal execution resumes. Any functions deferred byG
beforeD
are then run andG
'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演示了闭包的基本用法