看個例子, 這是一個讀取資料庫取資料的方法
func (db *DB) ReadData(age int, results []Result) {
// 查詢資料庫
// 錯誤, 釋放連線
// 取值反射錯誤, 釋放連線
// 成功, 釋放連線
}
因為GO沒有try{} finally{} 這語句.
所以很多情況如果要在離開函數之前, 作一些必要的動作時
就要在各種case下, 加上處理.
early return的寫法, 也要每個return前都寫一樣的處理, 破壞簡潔.
wtf 很容易寫成這樣 ... 只要邏輯的層數多點的話
But!!!
Go有Defer這延遲載入的語句!!!
剛剛的例子就能夠改成
func (db *DB) ReadData(age int, results []Result) {
// 查詢資料庫
defer 釋放連線
// 錯誤
// 取值反射錯誤
// 成功
}
來看看defer實際的存放跟執行順序先
defer 會被後面的執行語句, 依照後進先出LIFO的方式作執行,
至於defer被觸發的時間點, 就在當前函數返回之前就會被調用.
type _defer struct {
siz int32
started bool
sp uintptr
pc uintptr
fn *funcval
_panic *_panic
link *_defer
}
fn 存的就是指向defer關鍵字傳入的語句了
func main() {
fmt.Println("begin")
defer fmt.Print(1)
fmt.Println("do something")
defer fmt.Print(2)
fmt.Println("end")
}
/*
begin
do something
end
2
1
*/
也能傳入匿名函數
func main() {
fmt.Println("ithome")
defer func() {
fmt.Println("ironman")
fmt.Println("Day 13 post sucess")
}()
}
/*
ithome
ironman
Day 13 post sucess
*/
進階題 : defer 裡函數裡包著函數
func calc(index string, a, b int) int {
ret := a + b
fmt.Println(index, a, b, ret)
return ret
}
func main() {
a := 1
b := 2
// 記得是FILO
defer calc("1", a, calc("10", 2, b))
a = 0
defer calc("2", a, calc("20", a, b))
b = 1
}
/*
10 2 2 4
20 0 2 2
2 0 2 2
1 1 4 5
*/
func main() {
for i := 0; i < 5; i++ {
defer fmt.Println(i)
}
}
/*
4
3
2
1
0
*/
func fileSize(filename string) int64 {
// 根據文件名稱打開
f, err := os.Open(filename)
if err != nil {
// 嘗試開啟檔案的錯誤回傳, 不會觸發defer
return 0
}
// 宣告一個defer, 延遲調用Close(), 這時候還不會立刻被呼叫
defer f.Close()
// 獲取文件訊息
info, err := f.Stat()
if err != nil {
// 錯誤回傳, 觸發defer
return 0
}
// 獲取文件大小
size := info.Size()
// 回傳, 觸發defer
return size
}
var (
valueByKey = make(map[string]int)
valueByKeyGuard sync.Mutex
)
func readValue(key string) int {
valueByKeyGuard.Lock()
// 延遲解鎖
defer valueByKeyGuard.Unlocok()
return valueByKey[key]
}
func main() {
var run func() = nil
defer run()
fmt.Println("ithome")
}
/*
ithome
panic: runtime error: invalid memory address or nil pointer dereference
*/
func main() {
for {
row, err := db.Query("select 1")
if err != nil {
fmt.Println(err)
}
defer row.Close()
}
}
這種用法會在main這方法內, 一直累加很多個defer...
直到崩潰.
func main() {
for {
func() {
row, err := db.Query("select 1")
if err != nil {
fmt.Println(err)
}
defer row.Close()
}()
}
}
透過defer將匿名函數延遲執行,
panic觸發時, protectRun()函數就會結束, defer就會被觸發.
透過defer內的recoever捕捉到panic與其內容.
判斷是否是運行時的錯誤, 還是手動拋出的錯誤, 並作不同處置.
package main
import (
"fmt"
"runtime"
)
type panicContext struct {
function string
}
func protectRun(entry func()) {
defer func() {
if err := recover(); err != nil {
switch err.(type) {
case runtime.Error:
fmt.Println("runtime: ", err)
default:
fmt.Println("error : ", err)
}
}
}()
entry()
}
func main() {
fmt.Println("執行前")
protectRun(func() {
fmt.Println("手動觸發panic前")
panic(&panicContext{"手動觸發!"})
fmt.Println("手動觸發panic後")
})
protectRun(func() {
fmt.Println("賦值當機前")
var a *int
*a = 1
fmt.Println("賦值當機後")
})
fmt.Println("執行後")
}
/*
執行前
手動觸發panic前
error : &{手動觸發!}
賦值當機前
runtime: runtime error: invalid memory address or nil pointer dereference
執行後
*/
分享這個是因為...
未來很多真正使用上都會需要defer跟錯誤處理.
大大您好,非常感謝您的文章講解,讓我對於defer有初步的認識,
其中比較好奇的是關於您的進階題 : defer 裡函數裡包著函數 的部分,
因為根據您提到的說會FILO的來做執行,但剛好今天函數裡面再包函數來當作argument時,
它的實際執行的結果或順序是怎麼跑的呢?這點就有點讓我困惑了
希望大大有空能稍微提點一下,謝謝您:)
進入stack的內容指的是
defer 這關鍵字後, 每一個都是無法拆分的單元, 不管是命令還是一組func, 單元裡面就算執行很多命令, 也算成一組;
像我這樣其實是兩組東西塞入defer的執行stack
非常謝謝大大的回覆!!
所以他會是 defer cacl("2")-defer calc("1")-stackRoot 囉?
那如果是這樣的話,cacl("2")被pop出來執行時,我覺得應該是:
20 0 2 2
2 0 2 2
10 2 2 4
1 1 4 5
可是就執行結果上來看,為什麼他會是
10 2 2 4 這個先執行然後再去執行 cacl("2") 的部分,最後再回去執行 cacl("1")呢? 這執行順序的部分有點小卡牆...抱歉
也感謝大大的再次回復!!
https://play.golang.org/p/nctaxAoD3N_d
先來簡單的回, calc("1", a, calc("10", 2, b));
這裡Go在defer遇到func, 會先去確認argument的值, 也就是calc("10", 2, b) 其實就先被執行了.
他只是把結果帶到calc("1", a, 4) 直接變這樣了, 丟到stack內.
所以那兩句過場才會先被印XD, 根本那兩句過場跟cal(10)跟cal(20)都沒進stack, 但calc("10", 2, b)跟calc("20", a, b)的執行卻先跑了
小弟我在隔壁篇也有回到類似的概念
https://ithelp.ithome.com.tw/articles/10242498
這題目我知道蠻多公司的筆試出這題XD
都會先問defer的順序, 再來各種defer傳參數, defer傳方法, defer傳方法參數還有方法...
哈哈~原來如此~小弟受用並了解了,謝謝大大的回答:)