当前位置: 代码迷 >> 综合 >> golang内幕之defer panic recover
  详细解决方案

golang内幕之defer panic recover

热度:16   发布时间:2024-01-09 00:28:50.0

问题: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又正常的往下执行了。

在这里插入图片描述
在这里插入图片描述