问题: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 := 0i++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=0x280x0000 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, $10x003d 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 := 0defer 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=0x280x0000 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), AX0x005a 00090 (defer-panic-recover.go:10) INCQ AX0x005d 00093 (defer-panic-recover.go:10) MOVQ AX, "".i+24(SP)0x0062 00098 (defer-panic-recover.go:11) XCHGL AX, AX0x0063 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 := 0defer 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=0x280x0000 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), AX0x0059 00089 (defer-panic-recover.go:10) INCQ AX0x005c 00092 (defer-panic-recover.go:10) MOVQ AX, "".i+24(SP)0x0061 00097 (defer-panic-recover.go:11) XCHGL AX, AX0x0062 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 1for i := 0; i < 5; i++ {defer func() {fmt.Println("i=",i)}()}//loop 2for 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=0x380x0000 00000 (defer-panic-recover.go:5) TEXT "".DeferEval_4(SB), ABIInternal, $56-0...0x0028 00040 (defer-panic-recover.go:7) LEAQ type.int(SB), AX0x002f 00047 (defer-panic-recover.go:7) PCDATA $2, $00x002f 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, $00x0042 00066 (defer-panic-recover.go:7) MOVQ $0, (AX)0x0049 00073 (defer-panic-recover.go:7) JMP 750x004b 00075 (defer-panic-recover.go:7) PCDATA $2, $10x004b 00075 (defer-panic-recover.go:7) MOVQ "".&i+40(SP), AX0x0050 00080 (defer-panic-recover.go:7) PCDATA $2, $00x0050 00080 (defer-panic-recover.go:7) CMPQ (AX), $50x0054 00084 (defer-panic-recover.go:7) JLT 880x0056 00086 (defer-panic-recover.go:7) JMP 1720x0058 00088 (defer-panic-recover.go:8) PCDATA $2, $10x0058 00088 (defer-panic-recover.go:8) MOVQ "".&i+40(SP), AX0x005d 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, $20x0069 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, $00x0075 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, AX0x0081 00129 (defer-panic-recover.go:8) JNE 1560x0083 00131 (defer-panic-recover.go:8) JMP 1330x0085 00133 (defer-panic-recover.go:7) PCDATA $2, $-20x0085 00133 (defer-panic-recover.go:7) PCDATA $0, $-20x0085 00133 (defer-panic-recover.go:7) JMP 1350x0087 00135 (defer-panic-recover.go:7) PCDATA $2, $10x0087 00135 (defer-panic-recover.go:7) PCDATA $0, $10x0087 00135 (defer-panic-recover.go:7) MOVQ "".&i+40(SP), AX0x008c 00140 (defer-panic-recover.go:7) PCDATA $2, $00x008c 00140 (defer-panic-recover.go:7) MOVQ (AX), AX0x008f 00143 (defer-panic-recover.go:7) PCDATA $2, $30x008f 00143 (defer-panic-recover.go:7) MOVQ "".&i+40(SP), CX0x0094 00148 (defer-panic-recover.go:7) INCQ AX0x0097 00151 (defer-panic-recover.go:7) PCDATA $2, $00x0097 00151 (defer-panic-recover.go:7) MOVQ AX, (CX)0x009a 00154 (defer-panic-recover.go:7) JMP 750x009c 00156 (defer-panic-recover.go:8) PCDATA $0, $00x009c 00156 (defer-panic-recover.go:8) XCHGL AX, AX0x009d 00157 (defer-panic-recover.go:8) CALL runtime.deferreturn(SB)0x00a2 00162 (defer-panic-recover.go:8) MOVQ 48(SP), BP0x00a7 00167 (defer-panic-recover.go:8) ADDQ $56, SP0x00ab 00171 (defer-panic-recover.go:8) RET0x00ac 00172 (defer-panic-recover.go:14) MOVQ $0, "".j+24(SP)0x00b5 00181 (defer-panic-recover.go:14) JMP 1830x00b7 00183 (defer-panic-recover.go:14) CMPQ "".j+24(SP), $50x00bd 00189 (defer-panic-recover.go:14) JLT 1930x00bf 00191 (defer-panic-recover.go:14) JMP 2660x00c1 00193 (defer-panic-recover.go:15) MOVL $8, (SP)0x00c8 00200 (defer-panic-recover.go:15) PCDATA $2, $10x00c8 00200 (defer-panic-recover.go:15) LEAQ "".DeferEval_4.func2·f(SB), AX0x00cf 00207 (defer-panic-recover.go:15) PCDATA $2, $00x00cf 00207 (defer-panic-recover.go:15) MOVQ AX, 8(SP)0x00d4 00212 (defer-panic-recover.go:15) MOVQ "".j+24(SP), CX0x00d9 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, AX0x00e5 00229 (defer-panic-recover.go:15) JNE 2500x00e7 00231 (defer-panic-recover.go:15) JMP 2330x00e9 00233 (defer-panic-recover.go:14) PCDATA $2, $-20x00e9 00233 (defer-panic-recover.go:14) PCDATA $0, $-20x00e9 00233 (defer-panic-recover.go:14) JMP 2350x00eb 00235 (defer-panic-recover.go:14) PCDATA $2, $00x00eb 00235 (defer-panic-recover.go:14) PCDATA $0, $00x00eb 00235 (defer-panic-recover.go:14) MOVQ "".j+24(SP), AX0x00f0 00240 (defer-panic-recover.go:14) INCQ AX0x00f3 00243 (defer-panic-recover.go:14) MOVQ AX, "".j+24(SP)0x00f8 00248 (defer-panic-recover.go:14) JMP 1830x00fa 00250 (defer-panic-recover.go:15) XCHGL AX, AX0x00fb 00251 (defer-panic-recover.go:15) CALL runtime.deferreturn(SB)0x0100 00256 (defer-panic-recover.go:15) MOVQ 48(SP), BP0x0105 00261 (defer-panic-recover.go:15) ADDQ $56, SP0x0109 00265 (defer-panic-recover.go:15) RET0x010a 00266 (defer-panic-recover.go:19) XCHGL AX, AX0x010b 00267 (defer-panic-recover.go:19) CALL runtime.deferreturn(SB)0x0110 00272 (defer-panic-recover.go:19) MOVQ 48(SP), BP0x0115 00277 (defer-panic-recover.go:19) ADDQ $56, SP0x0119 00281 (defer-panic-recover.go:19) RET0x011a 00282 (defer-panic-recover.go:19) NOP0x011a 00282 (defer-panic-recover.go:5) PCDATA $0, $-10x011a 00282 (defer-panic-recover.go:5) PCDATA $2, $-10x011a 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=0x200x0000 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), AX0x0045 00069 (defer-real-time.go:4) PCDATA $2, $00x0045 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, AX0x005f 00095 (defer-real-time.go:7) CALL runtime.deferreturn(SB)0x0064 00100 (defer-real-time.go:7) MOVQ 24(SP), BP0x0069 00105 (defer-real-time.go:7) ADDQ $32, SP0x006d 00109 (defer-real-time.go:7) RET0x006e 00110 (defer-real-time.go:4) XCHGL AX, AX0x006f 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=0x00x0000 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), CX0x000d 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 mainfunc main() {Panic_1()
}
"".Panic_1 STEXT size=178 args=0x0 locals=0x680x0000 00000 (panic-builtin.go:5) TEXT "".Panic_1(SB), ABIInternal, $104-0...0x0048 00072 (panic-builtin.go:6) LEAQ "".statictmp_0(SB), CX0x004f 00079 (panic-builtin.go:6) PCDATA $2, $10x004f 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, $00x006f 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, $10x008a 00138 (panic-builtin.go:7) LEAQ type.string(SB), AX...0x0095 00149 (panic-builtin.go:7) LEAQ "".statictmp_1(SB), AX0x009c 00156 (panic-builtin.go:7) PCDATA $2, $00x009c 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) UNDEF0x00a8 00168 (panic-builtin.go:5) CALL runtime.morestack_noctxt(SB)0x00ad 00173 (panic-builtin.go:5) JMP 0
...
"".statictmp_1 SRODATA size=160x0000 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 exceptiongoroutine 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 exceptionProcess 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_1Process 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_5goroutine 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 +0x46Process 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 endProcess finished with exit code 0
可以看出,在Panic_4_2调用了recover捕获异常,所以就没往上抛了,即Panic_4_1、Panic_4、main又正常的往下执行了。