目錄
defer
調用時機
多次調用時的執行順序
傳參問題
源代碼
panic
執行defer
跨協程問題
源代碼
recover
使用
源代碼
總結
defer
panic
recover
全部源代碼
參考
本文進行了關鍵字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中使用時才有效
全部源代碼
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語言中,為什麼使用defer?
go-1.16.3源代碼
更多Go相關内容:Go-Golang學習總結筆記
有問題請下方評論,轉載請注明出處,并附有原文連結,謝謝!如有侵權,請及時聯系。如果您感覺有所收獲,自願打賞,可選擇支付寶18833895206(小于),您的支援是我不斷更新的動力。