<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
defer關鍵字可以讓函數或語句延遲到函數語句塊的最結尾時,即即將退出函數時執行,即便函數中途報錯結束、即便已經panic()、即便函數已經return了,也都會執行defer所推遲的物件。
其實defer的本質是,當在某個函數中使用了defer關鍵字,則建立一個獨立的defer棧幀,並將該defer語句壓入棧中,同時將其使用的相關變數也拷貝到該棧幀中(顯然是按值拷貝的)。因為棧是LIFO方式,所以先壓棧的後執行。因為是獨立的棧幀,所以即使呼叫者函數已經返回或報錯,也一樣能在它們之後進入defer棧幀去執行。
例如:
func main() { a() } func a() { println("in a") defer b() // 將b()壓入defer棧中 println("leaving a") //到了這裡才會執行b() } func b() { println("in b") println("leaving b") }
上面將輸出:
in a leaving a in b leaving b
即便是函數已經報錯,或函數已經return返回,defer的物件也會在函數退出前的最後一刻執行。
func a() TYPE{ ...CODE... defer b() ...CODE... // 函數執行出了錯誤 return args // 函數b()都會在這裡執行 }
但注意,由於Go的作用域採用的是詞法作用域,defer的定義位置決定了它推遲物件能看見的變數值,而不是推遲物件被呼叫時所能看見的值。
例如:
package main var x = 10 func main() { a() } func a() { println("start a:",x) // 輸出10 x = 20 defer b(x) // 壓棧,並按值拷貝20到棧中 x = 30 println("leaving a:",x) // 輸出30 // 呼叫defer延遲的物件b(),輸出20 } func b(x int) { println("start b:",x) }
比較下面的defer:
package main var x = 10 func main() { a() } func a() int { println("start a:", x) // 輸出10 x = 20 defer func() { // 壓棧,但並未傳值,所以內部參照x println("in defer:", x) // 輸出30 }() x = 30 println("leaving a:", x) // 輸出30 return x }
上面defer推遲的匿名函數輸出的值是30,它看見的不應該是20嗎?先再改成下面的:
package main var x = 10 func main() { a() } func a() int { println("start a:", x) // 輸出10 x = 20 defer func(x int) { println("in defer:", x) // 輸出20 }(x) x = 30 println("leaving a:", x) // 輸出30 return x }
這個defer推遲的物件中看見的卻是20,這和第一種defer b(x)
是相同的。
原因在於defer推遲的如果是函數,它直接就在它的定義位置處評估好引數、變數。該拷貝傳值的拷貝傳值,該指標相見的指標相見。所以,對於第(1)和第(3)種情況,在defer的定義位置處,就將x=20拷貝給了推遲的函數引數,所以函數內部操作的一直是x的副本。而第二種情況則是直接指向它所看見的x=20那個變數,則個變數是全域性變數,當執行x=30的時候會將其值修改,到執行defer推遲的物件時,它指向的x的值已經是修改過的。
再看下面這個例子,將defer放進一個語句塊中,並在這個語句塊中新宣告一個同名變數x:
func a() int { println("start a:", x) // 輸出10 x = 20 { x := 40 defer func() { println("in defer:", x) // 輸出40 }() } x = 30 println("leaving a:", x) // 輸出30 return x }
上面的defer定義在語句塊中,它能看見的x是語句塊中x=40
,它的x指向的是語句塊中的x。另一方面,當語句塊結束時,x=40
的x會消失,但由於defer的函數中仍有x指向40這個值,所以40這個值仍被defer的函數參照著,它直到defer執行完之後才會被GC回收。所以defer的函數在執行的時候,仍然會輸出40。
如果語句塊內有多個defer,則defer的物件以LIFO(last in first out)的方式執行,也就是說,先定義的defer後執行。
func main() { println("start...") defer println("1") defer println("2") defer println("3") defer println("4") println("end...") }
將輸出:
start... end... 4 3 2 1
defer有什麼用呢?一般用來做善後操作,例如清理垃圾、釋放資源,無論是否報錯都執行defer物件。另一方面,defer可以讓這些善後操作的語句和開始語句放在一起,無論在可讀性上還是安全性上都很有改善,畢竟寫完開始語句就可以直接寫defer語句,永遠也不會忘記關閉、善後等操作。
例如,開啟檔案,關閉檔案的操作寫在一起:
open() defer file.Close() ... 操作檔案 ...
以下是defer的一些常用場景:
panic()用於產生錯誤資訊並終止當前的goroutine,一般將其看作是退出panic()所在函數以及退出呼叫panic()所在函數的函數。例如,G()中呼叫F(),F()中呼叫panic(),則F()退出,G()也退出。
注意,defer關鍵字推遲的物件是函數最後呼叫的,即使出現了panic也會呼叫defer推遲的物件。
例如,下面的程式碼中,main()中輸出一個start main
之後呼叫a(),它會輸出start a
,然後就panic了,panic()會輸出panic: panic in a
,然後報錯,終止程式。
func main() { println("start main") a() println("end main") } func a() { println("start a") panic("panic in a") println("end a") }
執行結果如下:
start main start a panic: panic in a goroutine 1 [running]: main.a() E:/learning/err.go:14 +0x63 main.main() E:/learning/err.go:8 +0x4c exit status 2
注意上面的end a
和end main
都沒有被輸出。
可以使用recover()去捕獲panic()並恢復執行。recover()用於捕捉panic()錯誤,並返回這個錯誤資訊。但注意,即使recover()捕獲到了panic(),但呼叫含有panic()函數的函數(即上面的G()函數)也會退出,所以如果recover()定義在G()中,則G()中呼叫F()函數之後的程式碼都不會執行(見下面的通用格式)。
以下是比較通用的panic()和recover()的格式:
func main() { G() // 下面的程式碼會執行 ...CODE IN MAIN... } func G(){ defer func (){ if str := recover(); str != nil { fmt.Println(str) } }() ...CODE IN G()... // F()的呼叫必須在defer關鍵字之後 F() // 該函數內下面的程式碼不會執行 ...CODE IN G()... } func F() { ...CODE1... panic("error found") // 下面的程式碼不會執行 ...CODE IN F()... }
可以使用recover()去捕獲panic()並恢復執行。但以下程式碼是錯誤的:
func main() { println("start main") a() println("end main") } func a() { println("start a") panic("panic in a") // 直接放在panic後是錯誤的 panic_str := recover() println(panic_str) println("end a") }
之所以錯誤,是因為panic()一出現就直接退出函數a()和main()了。要想recover()真正捕獲panic(),需要將recover()放在defer的推遲物件中,且defer的定義必須在panic()發生之前。
例如,下面是通用格式的範例:
package main import "fmt" func main() { println("start main") b() println("end main") } func a() { println("start a") panic("panic in a") println("end a") } func b() { println("start b") defer func() { if str := recover(); str != nil { fmt.Println(str) } }() a() println("end b") }
以下是輸出結果:
start main start b start a panic in a end main
注意上面的end b
、end a
都沒有被輸出,但是end main
輸出了。
panic()是內建的函數(在包builtin中),在log
包中也有一個Panic()函數,它呼叫Print()輸出資訊後,再呼叫panic()。go doc log Panic
一看便知:
$ go doc log Panic func Panic(v ...interface{}) Panic is equivalent to Print() followed by a call to panic().
更多關於 Go基礎教學系列之defer、panic和recover詳解 請檢視下面的相關連結
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45