问题:defer适用在什么场景?
我们先看下下面一段拷贝文件的代码:
func CopyFile_1(src, dst string) (wlen int64, err error) {
sfd, err := os.Open(src)
if err != nil {
return 0, err
}
dfd, err := os.Create(dst)
if err != nil {
sfd.Close()
return 0, err
}
wlen, err = io.Copy(dfd, sfd)
if err != nil {
sfd.Close()
sfd.Close()
return 0, err
}
sfd.Close()
sfd.Close()
return wlen, nil
}
有没有发现,当有错误发生了,函数在返回前总要对已经打开的文件资源进行回收,即Close操作。
使用defer优化的代码如下:
func CopyFile_2(src, dst string) (wlen int64, err error) {
sfd, err := os.Open(src)
if err != nil {
return 0, err
}
defer sfd.Close()
dfd, err := os.Create(dst)
if err != nil {
return 0, err
}
defer dfd.Close()
wlen, err = io.Copy(dfd, sfd)
if err != nil {
return 0, err
}
return wlen, nil
}
相比之下,少了很多Close的操作代码。
总结:这就是defer的适用场景之一,在函数返回前,做一些回收工作。
func DeferEval_1() {
i := 0
i++
defer func() {
fmt.Println("i=", i)
}()
}
估计你能代码输出的是多少,没错就是1
=== RUN TestDeferEval_1
i= 1
--- PASS: TestDeferEval_1 (0.00s)
PASS
"".DeferEval_1 STEXT size=135 args=0x0 locals=0x28
0x0000 00000 (defer-panic-recover.go:5) TEXT "".DeferEval_1(SB), ABIInternal, $40-0
...
0x0024 00036 (defer-panic-recover.go:6) MOVQ $0, "".i+24(SP)
0x002d 00045 (defer-panic-recover.go:7) MOVQ $1, "".i+24(SP)
0x0036 00054 (defer-panic-recover.go:8) MOVL $8, (SP)
0x003d 00061 (defer-panic-recover.go:8) PCDATA $2, $1
0x003d 00061 (defer-panic-recover.go:8) LEAQ "".DeferEval_1.func1·f(SB), AX
...
0x0052 00082 (defer-panic-recover.go:8) CALL runtime.deferproc(SB)
\ ...
0x005e 00094 (defer-panic-recover.go:11) CALL runtime.deferreturn(SB)
...
0x006e 00110 (defer-panic-recover.go:8) CALL runtime.deferreturn(SB)
...
0x007d 00125 (defer-panic-recover.go:5) CALL runtime.morestack_noctxt(SB)
0x0082 00130 (defer-panic-recover.go:5) JMP 0
从汇编可以看出,由于i只有go function一处引用,因此只需值传递即可,并没有取i的地址,因为go function后并没有对i操作的代码。
func DeferEval_2() {
i := 0
defer func() {
fmt.Println("i=", i)
}()
i++
}
换了一下defer func和i++的位置,结果又会如何呢?
=== RUN TestDeferEval_2
i= 1
--- PASS: TestDeferEval_2 (0.00s)
PASS
"".DeferEval_2 STEXT size=140 args=0x0 locals=0x28
0x0000 00000 (defer-panic-recover.go:5) TEXT "".DeferEval_2(SB), ABIInternal, $40-0
...
0x0024 00036 (defer-panic-recover.go:6) MOVQ $0, "".i+24(SP)
...
0x0034 00052 (defer-panic-recover.go:7) LEAQ "".DeferEval_2.func1·f(SB), AX
...
0x0040 00064 (defer-panic-recover.go:7) LEAQ "".i+24(SP), AX
...
0x004a 00074 (defer-panic-recover.go:7) CALL runtime.deferproc(SB)
...
0x0055 00085 (defer-panic-recover.go:10) MOVQ "".i+24(SP), AX
0x005a 00090 (defer-panic-recover.go:10) INCQ AX
0x005d 00093 (defer-panic-recover.go:10) MOVQ AX, "".i+24(SP)
0x0062 00098 (defer-panic-recover.go:11) XCHGL AX, AX
0x0063 00099 (defer-panic-recover.go:11) CALL runtime.deferreturn(SB)
...
0x0073 00115 (defer-panic-recover.go:7) CALL runtime.deferreturn(SB)
...
0x0082 00130 (defer-panic-recover.go:5) CALL runtime.morestack_noctxt(SB)
0x0087 00135 (defer-panic-recover.go:5) JMP 0
汇编看出,取了i的地址作为参数传入(LEAQ "".i+24(SP), AX)。
func DeferEval_3() {
i := 0
defer func(i int) {
fmt.Println("i=", i)
}(i)
i++
}
=== RUN TestDeferEval_3
i= 0
--- PASS: TestDeferEval_3 (0.00s)
PASS
"".DeferEval_3 STEXT size=139 args=0x0 locals=0x28
0x0000 00000 (defer-panic-recover.go:5) TEXT "".DeferEval_3(SB), ABIInternal, $40-0
...
0x0024 00036 (defer-panic-recover.go:6) MOVQ $0, "".i+24(SP)
...
0x0034 00052 (defer-panic-recover.go:7) LEAQ "".DeferEval_3.func1·f(SB), AX
...
0x0049 00073 (defer-panic-recover.go:7) CALL runtime.deferproc(SB)
...
0x0054 00084 (defer-panic-recover.go:10) MOVQ "".i+24(SP), AX
0x0059 00089 (defer-panic-recover.go:10) INCQ AX
0x005c 00092 (defer-panic-recover.go:10) MOVQ AX, "".i+24(SP)
0x0061 00097 (defer-panic-recover.go:11) XCHGL AX, AX
0x0062 00098 (defer-panic-recover.go:11) CALL runtime.deferreturn(SB)
...
0x0072 00114 (defer-panic-recover.go:7) CALL runtime.deferreturn(SB)
...
0x0081 00129 (defer-panic-recover.go:5) CALL runtime.morestack_noctxt(SB)
0x0086 00134 (defer-panic-recover.go:5) JMP 0
并没有取i的地址,跟普通股函数传值一致。
总结:defer func总是在调用函数返回前执行,我们可以传参数进func,如TestDeferEval_3,此时传入的值就是0,因此打印出来的接口就是0;而TestDeferEval_1和TestDeferEval_2没给func传如任何参数,直接使用调用函数内声明的i,i在go function还有引用操作,估计传的是地址。
我们写一个稍微复杂点的例子:
func DeferEval_4() {
//loop 1
for i := 0; i < 5; i++ {
defer func() {
fmt.Println("i=",i)
}()
}
//loop 2
for j := 0; j < 5; j++ {
defer func(v int) {
fmt.Println("j=",v)
}(j)
}
}
这里引入两个loop的目的是为了分别给for循环声明的变量i和j创建两个不同的作用域,i属于loop 1,当最后一次i++,即为5时候,不满足条件,此时就会退出for循环,此时也是defer func计算i的值的时刻;j属于loop 2,由于defer func采用了传参,因此j是实时计算的,即j=0时,v=0,j=1时,v=1...。按照我们的结论应该是j会输出0,1,2,3,4,而i会输出5,5,5,5,5。
运行结果:
=== RUN TestDeferEval_4
j= 4
j= 3
j= 2
j= 1
j= 0
i= 5
i= 5
i= 5
i= 5
i= 5
--- PASS: TestDeferEval_4 (0.00s)
PASS
"".DeferEval_4 STEXT size=292 args=0x0 locals=0x38
0x0000 00000 (defer-panic-recover.go:5) TEXT "".DeferEval_4(SB), ABIInternal, $56-0
...
0x0028 00040 (defer-panic-recover.go:7) LEAQ type.int(SB), AX
0x002f 00047 (defer-panic-recover.go:7) PCDATA $2, $0
0x002f 00047 (defer-panic-recover.go:7) MOVQ AX, (SP)
0x0033 00051 (defer-panic-recover.go:7) CALL runtime.newobject(SB)
...
0x0038 00056 (defer-panic-recover.go:7) MOVQ 8(SP), AX
...
0x003d 00061 (defer-panic-recover.go:7) MOVQ AX, "".&i+40(SP)
0x0042 00066 (defer-panic-recover.go:7) PCDATA $2, $0
0x0042 00066 (defer-panic-recover.go:7) MOVQ $0, (AX)
0x0049 00073 (defer-panic-recover.go:7) JMP 75
0x004b 00075 (defer-panic-recover.go:7) PCDATA $2, $1
0x004b 00075 (defer-panic-recover.go:7) MOVQ "".&i+40(SP), AX
0x0050 00080 (defer-panic-recover.go:7) PCDATA $2, $0
0x0050 00080 (defer-panic-recover.go:7) CMPQ (AX), $5
0x0054 00084 (defer-panic-recover.go:7) JLT 88
0x0056 00086 (defer-panic-recover.go:7) JMP 172
0x0058 00088 (defer-panic-recover.go:8) PCDATA $2, $1
0x0058 00088 (defer-panic-recover.go:8) MOVQ "".&i+40(SP), AX
0x005d 00093 (defer-panic-recover.go:10) MOVQ AX, ""..autotmp_5+32(SP)
0x0062 00098 (defer-panic-recover.go:8) MOVL $8, (SP)
0x0069 00105 (defer-panic-recover.go:8) PCDATA $2, $2
0x0069 00105 (defer-panic-recover.go:8) LEAQ "".DeferEval_4.func1·f(SB), CX
...
0x0070 00112 (defer-panic-recover.go:8) MOVQ CX, 8(SP)
0x0075 00117 (defer-panic-recover.go:8) PCDATA $2, $0
0x0075 00117 (defer-panic-recover.go:8) MOVQ AX, 16(SP)
0x007a 00122 (defer-panic-recover.go:8) CALL runtime.deferproc(SB)
0x007f 00127 (defer-panic-recover.go:8) TESTL AX, AX
0x0081 00129 (defer-panic-recover.go:8) JNE 156
0x0083 00131 (defer-panic-recover.go:8) JMP 133
0x0085 00133 (defer-panic-recover.go:7) PCDATA $2, $-2
0x0085 00133 (defer-panic-recover.go:7) PCDATA $0, $-2
0x0085 00133 (defer-panic-recover.go:7) JMP 135
0x0087 00135 (defer-panic-recover.go:7) PCDATA $2, $1
0x0087 00135 (defer-panic-recover.go:7) PCDATA $0, $1
0x0087 00135 (defer-panic-recover.go:7) MOVQ "".&i+40(SP), AX
0x008c 00140 (defer-panic-recover.go:7) PCDATA $2, $0
0x008c 00140 (defer-panic-recover.go:7) MOVQ (AX), AX
0x008f 00143 (defer-panic-recover.go:7) PCDATA $2, $3
0x008f 00143 (defer-panic-recover.go:7) MOVQ "".&i+40(SP), CX
0x0094 00148 (defer-panic-recover.go:7) INCQ AX
0x0097 00151 (defer-panic-recover.go:7) PCDATA $2, $0
0x0097 00151 (defer-panic-recover.go:7) MOVQ AX, (CX)
0x009a 00154 (defer-panic-recover.go:7) JMP 75
0x009c 00156 (defer-panic-recover.go:8) PCDATA $0, $0
0x009c 00156 (defer-panic-recover.go:8) XCHGL AX, AX
0x009d 00157 (defer-panic-recover.go:8) CALL runtime.deferreturn(SB)
0x00a2 00162 (defer-panic-recover.go:8) MOVQ 48(SP), BP
0x00a7 00167 (defer-panic-recover.go:8) ADDQ $56, SP
0x00ab 00171 (defer-panic-recover.go:8) RET
0x00ac 00172 (defer-panic-recover.go:14) MOVQ $0, "".j+24(SP)
0x00b5 00181 (defer-panic-recover.go:14) JMP 183
0x00b7 00183 (defer-panic-recover.go:14) CMPQ "".j+24(SP), $5
0x00bd 00189 (defer-panic-recover.go:14) JLT 193
0x00bf 00191 (defer-panic-recover.go:14) JMP 266
0x00c1 00193 (defer-panic-recover.go:15) MOVL $8, (SP)
0x00c8 00200 (defer-panic-recover.go:15) PCDATA $2, $1
0x00c8 00200 (defer-panic-recover.go:15) LEAQ "".DeferEval_4.func2·f(SB), AX
0x00cf 00207 (defer-panic-recover.go:15) PCDATA $2, $0
0x00cf 00207 (defer-panic-recover.go:15) MOVQ AX, 8(SP)
0x00d4 00212 (defer-panic-recover.go:15) MOVQ "".j+24(SP), CX
0x00d9 00217 (defer-panic-recover.go:15) MOVQ CX, 16(SP)
0x00de 00222 (defer-panic-recover.go:15) CALL runtime.deferproc(SB)
0x00e3 00227 (defer-panic-recover.go:15) TESTL AX, AX
0x00e5 00229 (defer-panic-recover.go:15) JNE 250
0x00e7 00231 (defer-panic-recover.go:15) JMP 233
0x00e9 00233 (defer-panic-recover.go:14) PCDATA $2, $-2
0x00e9 00233 (defer-panic-recover.go:14) PCDATA $0, $-2
0x00e9 00233 (defer-panic-recover.go:14) JMP 235
0x00eb 00235 (defer-panic-recover.go:14) PCDATA $2, $0
0x00eb 00235 (defer-panic-recover.go:14) PCDATA $0, $0
0x00eb 00235 (defer-panic-recover.go:14) MOVQ "".j+24(SP), AX
0x00f0 00240 (defer-panic-recover.go:14) INCQ AX
0x00f3 00243 (defer-panic-recover.go:14) MOVQ AX, "".j+24(SP)
0x00f8 00248 (defer-panic-recover.go:14) JMP 183
0x00fa 00250 (defer-panic-recover.go:15) XCHGL AX, AX
0x00fb 00251 (defer-panic-recover.go:15) CALL runtime.deferreturn(SB)
0x0100 00256 (defer-panic-recover.go:15) MOVQ 48(SP), BP
0x0105 00261 (defer-panic-recover.go:15) ADDQ $56, SP
0x0109 00265 (defer-panic-recover.go:15) RET
0x010a 00266 (defer-panic-recover.go:19) XCHGL AX, AX
0x010b 00267 (defer-panic-recover.go:19) CALL runtime.deferreturn(SB)
0x0110 00272 (defer-panic-recover.go:19) MOVQ 48(SP), BP
0x0115 00277 (defer-panic-recover.go:19) ADDQ $56, SP
0x0119 00281 (defer-panic-recover.go:19) RET
0x011a 00282 (defer-panic-recover.go:19) NOP
0x011a 00282 (defer-panic-recover.go:5) PCDATA $0, $-1
0x011a 00282 (defer-panic-recover.go:5) PCDATA $2, $-1
0x011a 00282 (defer-panic-recover.go:5) CALL runtime.morestack_noctxt(SB)
0x011f 00287 (defer-panic-recover.go:5) JMP 0
可以看出,i取了地址值传入,j普通传值传入。
总结:如果按照函数传值传入,按传入的值处理,如果在go function内部直接使用调用函数变量,如果go function后仍会有操作调用函数变量的情况,则传入的则是变量的地址值。
i和j输出的结果符合我们的预期,但是输出的顺却有出入,但顺序我们接下来会探讨。
func DeferOrder_1() {
fmt.Println("0")
defer func() {
fmt.Println("4")
}()
defer func() {
fmt.Println("3")
}()
defer func() {
fmt.Println("2")
}()
fmt.Println("1")
}
之前说过,defer func是调用函数返回前执行的,那么先会输出0和1。然后defer func采用了先进后出,即会输出2,3,4。
=== RUN TestDeferOrder_1
0
1
2
3
4
--- PASS: TestDeferOrder_1 (0.00s)
PASS
func DeferRealTime_1() (ret int) {
defer func() {
ret++
}()
return 99
}
func TestDeferRealTime_1(t *testing.T) {
ret := DeferRealTime_1()
fmt.Println(ret)
}
=== RUN TestDeferRealTime_1
100
--- PASS: TestDeferRealTime_1 (0.00s)
PASS
"".DeferRealTime_1 STEXT size=136 args=0x8 locals=0x20
0x0000 00000 (defer-real-time.go:3) TEXT "".DeferRealTime_1(SB), ABIInternal, $32-8
...
0x0024 00036 (defer-real-time.go:3) MOVQ $0, "".ret+40(SP)
...
0x0034 00052 (defer-real-time.go:4) LEAQ "".DeferRealTime_1.func1·f(SB), AX
...
0x0040 00064 (defer-real-time.go:4) LEAQ "".ret+40(SP), AX
0x0045 00069 (defer-real-time.go:4) PCDATA $2, $0
0x0045 00069 (defer-real-time.go:4) MOVQ AX, 16(SP)
0x004a 00074 (defer-real-time.go:4) CALL runtime.deferproc(SB)
...
0x0055 00085 (defer-real-time.go:7) MOVQ $99, "".ret+40(SP)
0x005e 00094 (defer-real-time.go:7) XCHGL AX, AX
0x005f 00095 (defer-real-time.go:7) CALL runtime.deferreturn(SB)
0x0064 00100 (defer-real-time.go:7) MOVQ 24(SP), BP
0x0069 00105 (defer-real-time.go:7) ADDQ $32, SP
0x006d 00109 (defer-real-time.go:7) RET
0x006e 00110 (defer-real-time.go:4) XCHGL AX, AX
0x006f 00111 (defer-real-time.go:4) CALL runtime.deferreturn(SB)
...
0x007e 00126 (defer-real-time.go:3) CALL runtime.morestack_noctxt(SB)
0x0083 00131 (defer-real-time.go:3) JMP 0
"".DeferRealTime_1.func1 STEXT nosplit size=20 args=0x8 locals=0x0
0x0000 00000 (defer-real-time.go:4) TEXT "".DeferRealTime_1.func1(SB), NOSPLIT|ABIInternal, $0-8
...
0x0000 00000 (defer-real-time.go:5) MOVQ "".&ret+8(SP), AX
...
0x0008 00008 (defer-real-time.go:5) MOVQ "".&ret+8(SP), CX
0x000d 00013 (defer-real-time.go:5) INCQ AX
...
0x0013 00019 (defer-real-time.go:6) RET
取地址传入,然后在go function使用地址值变量进行加1操作。
这才是真是调用函数返回的时机。
接下来,探讨一下panic:
func Panic_1() {
fmt.Println("0")
panic("rise exception")
fmt.Println("1")
}
package main
func main() {
Panic_1()
}
"".Panic_1 STEXT size=178 args=0x0 locals=0x68
0x0000 00000 (panic-builtin.go:5) TEXT "".Panic_1(SB), ABIInternal, $104-0
...
0x0048 00072 (panic-builtin.go:6) LEAQ "".statictmp_0(SB), CX
0x004f 00079 (panic-builtin.go:6) PCDATA $2, $1
0x004f 00079 (panic-builtin.go:6) MOVQ CX, ""..autotmp_0+64(SP)
..
0x0058 00088 (panic-builtin.go:6) MOVQ AX, ""..autotmp_1+72(SP)
0x005d 00093 (panic-builtin.go:6) MOVQ $1, ""..autotmp_1+80(SP)
0x0066 00102 (panic-builtin.go:6) MOVQ $1, ""..autotmp_1+88(SP)
0x006f 00111 (panic-builtin.go:6) PCDATA $2, $0
0x006f 00111 (panic-builtin.go:6) MOVQ AX, (SP)
0x0073 00115 (panic-builtin.go:6) MOVQ $1, 8(SP)
0x007c 00124 (panic-builtin.go:6) MOVQ $1, 16(SP)
0x0085 00133 (panic-builtin.go:6) CALL fmt.Println(SB)
0x008a 00138 (panic-builtin.go:7) PCDATA $2, $1
0x008a 00138 (panic-builtin.go:7) LEAQ type.string(SB), AX
...
0x0095 00149 (panic-builtin.go:7) LEAQ "".statictmp_1(SB), AX
0x009c 00156 (panic-builtin.go:7) PCDATA $2, $0
0x009c 00156 (panic-builtin.go:7) MOVQ AX, 8(SP)
0x00a1 00161 (panic-builtin.go:7) CALL runtime.gopanic(SB)
0x00a6 00166 (panic-builtin.go:7) UNDEF
0x00a8 00168 (panic-builtin.go:5) CALL runtime.morestack_noctxt(SB)
0x00ad 00173 (panic-builtin.go:5) JMP 0
...
"".statictmp_1 SRODATA size=16
0x0000 00 00 00 00 00 00 00 00 0e 00 00 00 00 00 00 00 ................
rel 0+8 t=1 go.string."rise exception"+0
panic是golang内置的一个函数,编译时翻译成gopanic。
panic发生了,还会正常往下执行吗?
0
panic: rise exception
goroutine 1 [running]:
main.Panic_1()
C:/Users/Administrator/go/src/mq30/defer-panic-recover.go:117 +0x9d
main.main()
C:/Users/Administrator/go/src/mq30/defer-panic-recover.go:122 +0x27
总结:很明显,当panic发生,不会继续往下执行代码。
当panic遇上recover,会发生什么呢?golang规定recover必须在defer中生效。
func Panic_2() {
fmt.Println("0")
defer func() {
err := recover()
if err != nil {
fmt.Println("catch exception")
}
}()
panic("rise exception")
fmt.Println("1")
}
func main() {
Panic_2()
}
0
catch exception
Process finished with exit code 0
如果有多个recover代码会怎样?
func Panic_3() {
fmt.Println("0")
defer func() {
err := recover()
if err != nil {
fmt.Println("catch exception_3")
}
}()
defer func() {
err := recover()
if err != nil {
fmt.Println("catch exception_2")
}
}()
defer func() {
err := recover()
if err != nil {
fmt.Println("catch exception_1")
}
}()
panic("rise exception")
fmt.Println("1")
}
0
catch exception_1
Process finished with exit code 0
总结:离panic最进的recover会捕获到异常,其他recover捕获不到了。
如果func1 -> func2 -> func3 -> func4 -> func5,func5中发生panic,但没通过recover进行恢复,会怎么样?
func Panic_4() {
Panic_4_1()
}
func Panic_4_1() {
Panic_4_2()
}
func Panic_4_2() {
Panic_4_3()
}
func Panic_4_3() {
Panic_4_4()
}
func Panic_4_4() {
Panic_4_5()
}
func Panic_4_5() {
panic("rise in func Panic_4_5")
}
func main() {
Panic_4()
}
panic: rise in func Panic_4_5
goroutine 1 [running]:
main.Panic_4_5(...)
C:/Users/Administrator/go/src/mq30/defer-panic-recover.go:178
main.Panic_4_4(...)
C:/Users/Administrator/go/src/mq30/defer-panic-recover.go:174
main.Panic_4_3(...)
C:/Users/Administrator/go/src/mq30/defer-panic-recover.go:170
main.Panic_4_2(...)
C:/Users/Administrator/go/src/mq30/defer-panic-recover.go:166
main.Panic_4_1(...)
C:/Users/Administrator/go/src/mq30/defer-panic-recover.go:162
main.Panic_4(...)
C:/Users/Administrator/go/src/mq30/defer-panic-recover.go:158
main.main()
C:/Users/Administrator/go/src/mq30/defer-panic-recover.go:182 +0x46
Process finished with exit code 2
我们可以在 Panic_4 Panic_4_?任意一个函数增加recover捕获异常代码,可以总结出:
当一个函数发生了panic,会检查当前调用函数是否有recover,没有的话,会往上抛,直到有recover,不然相当于一直会到main.main。
func Panic_4() {
fmt.Println("Panic_4 start")
Panic_4_1()
fmt.Println("Panic_4 end")
}
func Panic_4_1() {
fmt.Println("Panic_4_1 start")
Panic_4_2()
fmt.Println("Panic_4_1 end")
}
func Panic_4_2() {
defer func() {
if err := recover(); err != nil {
fmt.Println("Panic_4_2 recover, err:", err)
}
}()
fmt.Println("Panic_4_2 start")
Panic_4_3()
fmt.Println("Panic_4_2 end")
}
func Panic_4_3() {
fmt.Println("Panic_4_3 start")
Panic_4_4()
fmt.Println("Panic_4_3 end")
}
func Panic_4_4() {
fmt.Println("Panic_4_4 start")
Panic_4_5()
fmt.Println("Panic_4_4 end")
}
func Panic_4_5() {
fmt.Println("Panic_4_5 start")
panic("rise in func Panic_4_5")
fmt.Println("Panic_4_5 end")
}
func main() {
Panic_4()
}
Panic_4 start
Panic_4_1 start
Panic_4_2 start
Panic_4_3 start
Panic_4_4 start
Panic_4_5 start
Panic_4_2 recover, err: rise in func Panic_4_5
Panic_4_1 end
Panic_4 end
Process finished with exit code 0
可以看出,在Panic_4_2调用了recover捕获异常,所以就没往上抛了,即Panic_4_1、Panic_4、main又正常的往下执行了。