iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 13
2
Software Development

下班加減學點Golang與Docker系列 第 13

Defer 延遲調用

看個例子, 這是一個讀取資料庫取資料的方法

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被觸發的時間點, 就在當前函數返回之前就會被調用.

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]
}

誤用defer

defer 去執行nil

func main() {
	var run func() = nil

	defer run()

	fmt.Println("ithome")
}
/*
ithome
panic: runtime error: invalid memory address or nil pointer dereference
*/

for loop中使用

func main() {
    for {
        row, err := db.Query("select 1")
        if err != nil {
            fmt.Println(err)
        }
        defer row.Close()
    }
}

這種用法會在main這方法內, 一直累加很多個defer...
直到崩潰.

解法, 直接再開一個匿名函數, 就會在這匿名函數結束前執行defer

func main() {
    for {
        func() {
            row, err := db.Query("select 1")
            if err != nil {
                fmt.Println(err)
            }
            defer row.Close()
        }()
       
    }
}

panic恢復

透過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跟錯誤處理.

躲避 Go 1.13 defer 性能提升的姿势


上一篇
go modules 終於不會再被GOPATH綁死了
下一篇
Goroutine 讓你用少少的線程, 能接受更多的工作, 但沒說會作比較快
系列文
下班加減學點Golang與Docker30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

1
Noch
iT邦新手 5 級 ‧ 2021-06-03 15:01:32

大大您好,非常感謝您的文章講解,讓我對於defer有初步的認識,
其中比較好奇的是關於您的進階題 : defer 裡函數裡包著函數 的部分,
因為根據您提到的說會FILO的來做執行,但剛好今天函數裡面再包函數來當作argument時,
它的實際執行的結果或順序是怎麼跑的呢?這點就有點讓我困惑了
希望大大有空能稍微提點一下,謝謝您:)

看更多先前的回應...收起先前的回應...
雷N iT邦研究生 1 級 ‧ 2021-06-03 15:16:43 檢舉
雷N iT邦研究生 1 級 ‧ 2021-06-03 15:18:48 檢舉

進入stack的內容指的是
defer 這關鍵字後, 每一個都是無法拆分的單元, 不管是命令還是一組func, 單元裡面就算執行很多命令, 也算成一組;
像我這樣其實是兩組東西塞入defer的執行stack

Noch iT邦新手 5 級 ‧ 2021-06-03 16:36:31 檢舉

非常謝謝大大的回覆!!
所以他會是 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")呢? 這執行順序的部分有點小卡牆...抱歉
也感謝大大的再次回復!!

雷N iT邦研究生 1 級 ‧ 2021-06-03 17:12:33 檢舉

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)的執行卻先跑了

雷N iT邦研究生 1 級 ‧ 2021-06-03 17:14:38 檢舉

小弟我在隔壁篇也有回到類似的概念
https://ithelp.ithome.com.tw/articles/10242498

雷N iT邦研究生 1 級 ‧ 2021-06-03 17:27:46 檢舉

這題目我知道蠻多公司的筆試出這題XD
都會先問defer的順序, 再來各種defer傳參數, defer傳方法, defer傳方法參數還有方法...

Noch iT邦新手 5 級 ‧ 2021-06-03 21:32:48 檢舉

哈哈~原來如此~小弟受用並了解了,謝謝大大的回答:)/images/emoticon/emoticon02.gif

我要留言

立即登入留言