本文进行了关键字defer和内建函数panic、recover的介绍和使用细节。
defer
Go 语言的 defer 会在当前函数返回前执行传入的函数,它会经常被用于关闭文件描述符、关闭数据库连接以及解锁资源。
在文章Go-函数详解(参数、返回值、init函数、匿名函数、defer)中进行了简单的使用,但是还不够深入,于是在知乎提了问题,今天做下总结。
调用时机
代码
//------调用时机:所在函数结束或返回前------ func callTime() { { defer fmt.Println("defer in callTime()") fmt.Println("code block finish..") } fmt.Println("callTime() finish...") }
结果
code block finish..
callTime() finish...
defer in callTime()
调用时机在函数/方法结束或返回前
多次调用时的执行顺序
代码
func moreDefer() { for i:=1;i<5;i++{ defer fmt.Println("defer",i) } fmt.Println("moreDefer() finish...") }
结果
moreDefer() finish...
defer 4
defer 3
defer 2
defer 1
栈的顺序调用,先入后出。
传参问题
代码
func deferPara() { i := 0 defer fmt.Println("defer",i,"in deferPara()") i++ fmt.Println("deferPara finish...,i is ", i) }
或许你认为结果是这样的:
deferPara finish...,i is 1
defer 1 in deferPara()
因为defer在函数结束前运行嘛,但事实上结果是这样的:
deferPara finish...,i is 1 defer 0 in deferPara()
defer会在到达所在行时,就将变量复制一份传过去。想到的解决方案如下:
- 引用类型就没有问题了
- 如果参数是值类型,你的defer不修改参数,你可以传地址。
- 如果参数是值类型,你也可以将defer放在函数/方法不修改参数后。
- 如果参数是值类型,你可以使用匿名函数,函数体内再用参数。
func paraFix() { i := 0 defer fmt.Println("send addr:defer",&i,"in paraFix()") defer func() {fmt.Println("no name func: defer",i,"in paraFix()")}() i++ defer fmt.Println("put defer later:defer",i,"in deferPara()") }
源代码
结构体
src->runtime->runtime2.go
type _defer struct { siz int32 // 包含参数和结果 started bool // 是否开始 heap bool // 是否分配在堆上 openDefer bool // 是否开放编码 sp uintptr // 栈指针 pc uintptr // 程序计数器 fn *funcval // 开放编码时可为nil _panic *_panic // 正在运行的defer的panic link *_defer // _defer指针 fd unsafe.Pointer // 预分配的函数数据 varp uintptr framepc uintptr }
_defer是一个单链表(链栈),采用头插的方式,取的时候先取头的。
编译
src->cmd->compoile->internal->gc->ssa.go stmt方法的一个case
case ODEFER: if Debug_defer > 0 { var defertype string if s.hasOpenDefers { defertype = "open-coded" } else if n.Esc == EscNever { defertype = "stack-allocated" } else { defertype = "heap-allocated" } Warnl(n.Pos, "%s defer", defertype) } if s.hasOpenDefers { s.openDeferRecord(n.Left) } else { d := callDefer if n.Esc == EscNever { d = callDeferStack } s.callResult(n.Left, d) }
有些defer将在栈上分配,有些在堆上分配。首先是开放编码进行优化,其次是栈,最后是堆,分配到栈上可以节约内存分配带来的额外开销。
panic
执行defer
当panic异常发生时,程序会中断运行,并立即执行在该goroutine中被延迟的函数(defer机制)。随后,程序崩溃并输出日志信息。
func panicDefer() { fmt.Println("code before panic") defer fmt.Println("defer in panicDefer") panic("something wrong in panic Defer") fmt.Println("code after panic") }
结果
code before panic
defer in panicDefer
panic: something wrong in panic Defer
goroutine 1 [running]:
main.panicDefer()
E:/Workspace/Go_workspace/learn_go/src/defer_panic_recover/main/main.go:42 +0x10a
main.main()
E:/Workspace/Go_workspace/learn_go/src/defer_panic_recover/main/main.go:62 +0x27
exit status 2
利用defer就可以实现有panic时也能进行资源释放等。
跨协程问题
panic 只会触发当前 goroutine 的 defer
func panicGoroutine() { defer println("defer in main") go func() { defer println("defer in goroutine") panic("something wrong...") }() time.Sleep(time.Second) }
结果:
defer in goroutine
panic: something wrong...
goroutine 6 [running]:
main.panicGoroutine.func1()
E:/Workspace/Go_workspace/learn_go/src/defer_panic_recover/main/main.go:50 +0x78
created by main.panicGoroutine
E:/Workspace/Go_workspace/learn_go/src/defer_panic_recover/main/main.go:48 +0x78
exit status 2
协程外面的defer执行不了
源代码
结构体
src->runtime->runtime2.go
type _panic struct { argp unsafe.Pointer // 指向defer栈的函数指针 arg interface{} // panic参数 link *_panic // 先前panic的指针 pc uintptr // 运行时,此panic被绕过时返回到哪 sp unsafe.Pointer // 运行时,此panic被绕过时返回到哪 recovered bool // 是否此panic结束 aborted bool // 这个panic被终止 goexit bool }
崩溃
src->runtime->panic.go
可以查看gopanic、fatalpanic两个函数,代码过多,不黏贴了,有兴趣可以看看。下面是fatalpanic的部分代码:
systemstack(func() { exit(2) })
这就能看出前面panic时为什么是“exit status 2”了
recover
使用
recover 可以中止 panic 造成的程序崩溃,只能在 defer 中发挥作用
代码
func recoverDefer() { //defer println("defer in main") // 执行不到 defer func() { if err := recover();err!=nil{ println("defer in main") println(err) } }() go func() { defer println("defer in goroutine") panic("something wrong...") }() panic("something wrong in recoverDefer()") time.Sleep(time.Second) }
结果
defer in main
(0xf4d940,0xf85bc8)
defer in goroutine
源代码
src->runtime->panic.go
func gorecover(argp uintptr) interface{} { gp := getg() p := gp._panic if p != nil && !p.goexit && !p.recovered && argp == uintptr(p.argp) { p.recovered = true return p.arg } return nil }
可以看到,p!=nil的话才行,也就是说你在panic前使用recover,没在defer中,那么返回的是nil,也就是recover失效了。如果不是nil,在gopanic中会进行处理。
总结
defer
- 调用时机:函数或方法在返回或结束前执行
- 多次调用:先写的后调用,栈的顺序
- 传参问题:值类型时,参数不改变后使用defer、defer的函数不修改则传地址、匿名函数函数体中使用
优点:
- panic后执行defer,防止异常时忘记释放资源
- 函数复杂分支返回,写一次即可,简洁,复用性好
panic
- panic后执行本协程的defer
- 跨协程问题使用recover解决
recover
- 终止panic造成的崩溃
- 在defer中使用时才有效
全部源代码
1.package main import ( "fmt" "time" ) //------调用时机:所在函数结束或返回前------ func callTime() { { defer fmt.Println("defer in callTime()") fmt.Println("code block finish..") } fmt.Println("callTime() finish...") } //-----多个defer的顺序------ func moreDefer() { for i:=1;i<5;i++{ defer fmt.Println("defer",i) } fmt.Println("moreDefer() finish...") } //-----defer传参问题----- func deferPara() { i := 0 defer fmt.Println("defer",i,"in deferPara()") i++ fmt.Println("deferPara finish...,i is ", i) } //-----defer传参修复方案-------- func paraFix() { i := 0 defer fmt.Println("send addr:defer",&i,"in paraFix()") defer func() {fmt.Println("no name func: defer",i,"in paraFix()")}() i++ defer fmt.Println("put defer later:defer",i,"in deferPara()") } //----panic后执行defer------- func panicDefer() { fmt.Println("code before panic") defer fmt.Println("defer in panicDefer") panic("something wrong in panic Defer") fmt.Println("code after panic") } //----跨协程 defer panic问题----- func panicGoroutine() { defer println("defer in main") go func() { defer println("defer in goroutine") panic("something wrong...") }() time.Sleep(time.Second) } //----------defer中使用recover--------- func recoverDefer() { //defer println("defer in main") // 执行不到 defer func() { if err := recover();err!=nil{ println("defer in main") println(err) } }() go func() { defer println("defer in goroutine") panic("something wrong...") }() panic("something wrong in recoverDefer()") time.Sleep(time.Second) } func main() { //callTime() //moreDefer() //deferPara() //paraFix() //panicDefer() //panicGoroutine() recoverDefer() }
参考
go-1.16.3源代码
更多Go相关内容:Go-Golang学习总结笔记
有问题请下方评论,转载请注明出处,并附有原文链接,谢谢!如有侵权,请及时联系