Go 学习笔记(17)— 函数(03)[defer 定义、defer 特点、defer 释放资源]
1. defer 定義
Go 函數的關鍵字 defer 可以提供注冊多個延遲調用,只能出現在函數內部,在 defer 歸屬的函數即將返回時,將延遲處理的語句按 defer 的逆序進行執行,這些調用遵循先進后出的順序在函數返回前被執行。也就是說,先被 defer 的語句最后被執行,最后被 defer 的語句,最先被執行。
defer 常用于保證一些資源最終一定能夠得到釋放或者回收。
2. defer 使用
代碼示例:
package mainimport "fmt"func main() {defer func() {fmt.Println("First")}()defer func() {fmt.Println("Second")}() // defer 后面必須是函數或者方法的調用,否則報錯:// expression in defer must be function callfmt.Println("This is main func body")}
輸出:
This is main func body
Second
First
3. defer 特點
3.1 defer 實參使用值拷貝傳遞
defer 函數的實參在注冊時使用值拷貝傳遞進去,即 defer 后面的函數參數會被實時解析;
package mainimport "fmt"func main() {a := 10defer func(i int) {fmt.Println("defer func i is ", i) // defer func i is 10}(a)a += 10fmt.Println("after defer a is ", a) // after defer a is 20 }
可以看到后面的 a += 10 并不影響 defer 函數的結果。
3.2 defer 必須先注冊
defer 函數必須先注冊才能執行,如果 defer 位于 return 語句之后,因為 defer 沒有注冊,不會被執行;
package mainimport "fmt"func main() {defer func() {fmt.Println("First")}()return// 后面的均不會執行defer func() {fmt.Println("Second")}()fmt.Println("This is main func body")}
輸出:
First
3.3 defer 遇到 os.Exit
當主動調用 os.Exit(int) 退出進程時, defer 即使已經注冊,那么也不再被執行。
package mainimport ("fmt""os"
)func main() {defer func() {fmt.Println("First")}()fmt.Println("This is main func body")os.Exit(1)
}
輸出:
This is main func body
exit status 1
3.4 defer 語句放到錯誤檢查語句之后
一般 defer 語句放到錯誤檢查語句之后;
3.5 defer 盡量不要放到循環語句內部
defer 盡量不要放到循環語句內部;
3.6 defer 性能損耗
defer 相對普通函數調用有一定的性能損耗,具體參考 Go defer 會有性能損耗,盡量不能用?;
3.7 defer 用于資源釋放和錯誤處理
defer 通常用于釋放資源或錯誤處理。
package mainimport "os"func test() error {f, err := os.Create("test.txt")if err != nil {return err}defer f.Close() // 注冊調用,而不是注冊函數。必須提供參數,哪怕為空。f.WriteString("Hello, World!")return nil
}func main() {test()}
3.8 多個 defer 注冊,按 FILO 次序執行
多個 defer 注冊,按 FILO 次序執行。哪怕函數或某個延遲調用發生錯誤,這些調用依舊會被執行。
package mainfunc test(x int) {defer println("a")defer println("b")defer func() {println(100 / x) // div0 異常未被捕獲,逐步往外傳遞,最終終止進程。}()defer println("c")
}func main() {test(0)
}
輸出:
c
b
a
panic: runtime error: integer divide by zero
3.9 延遲調用參數在注冊時求值或復制
延遲調用參數在注冊時求值或復制,可用指針或閉包 “延遲” 讀取。
package mainfunc test() {x, y := 10, 20defer func(i int) {println("defer:", i, y) // y 閉包引用}(x) // x 被復制x += 10y += 100println("x =", x, "y =", y)
}func main() {test()
}
輸出:
x = 20 y = 120
defer: 10 120
defer 關鍵字后面的表達式,是在將 deferred 函數注冊到 deferred 函數棧的時候進行求值的。
我們同樣用一個典型的例子來說明一下 defer 后表達式的求值時機:
func foo1() {for i := 0; i <= 3; i++ {defer fmt.Println(i)}
}func foo2() {for i := 0; i <= 3; i++ {defer func(n int) {fmt.Println(n)}(i)}
}func foo3() {for i := 0; i <= 3; i++ {defer func() {fmt.Println(i)}()}
}func main() {fmt.Println("foo1 result:")foo1()fmt.Println("\nfoo2 result:")foo2()fmt.Println("\nfoo3 result:")foo3()
}
這里,我們一個個分析 foo1、foo2 和 foo3 中 defer 后的表達式的求值時機。
首先是 foo1。foo1 中 defer 后面直接用的是 fmt.Println 函數,每當 defer 將 fmt.Println 注冊到 deferred 函數棧的時候,都會對 Println 后面的參數進行求值。根據上述代碼邏輯,依次壓入 deferred 函數棧的函數是:
fmt.Println(0)
fmt.Println(1)
fmt.Println(2)
fmt.Println(3)
因此,當 foo1 返回后,deferred 函數被調度執行時,上述壓入棧的 deferred 函數將以 LIFO 次序出棧執行,這時的輸出的結果為:
3
2
1
0
然后我們再看 foo2。foo2 中 defer 后面接的是一個帶有一個參數的匿名函數。每當 defer 將匿名函數注冊到 deferred 函數棧的時候,都會對該匿名函數的參數進行求值。根據上述代碼邏輯,依次壓入 deferred 函數棧的函數是:
func(0)
func(1)
func(2)
func(3)
因此,當 foo2 返回后,deferred 函數被調度執行時,上述壓入棧的 deferred 函數將以 LIFO 次序出棧執行,因此輸出的結果為:
3
2
1
0
最后我們來看 foo3。foo3 中 defer 后面接的是一個不帶參數的匿名函數。根據上述代碼邏輯,依次壓入 deferred 函數棧的函數是:
func()
func()
func()
func()
所以,當 foo3 返回后,deferred 函數被調度執行時,上述壓入棧的 deferred 函數將以 LIFO 次序出棧執行。匿名函數會以閉包的方式訪問外圍函數的變量 i,并通過 Println 輸出 i 的值,此時 i 的值為 4,因此 foo3 的輸出結果為:
4
4
4
4
通過這些例子,我們可以看到,無論以何種形式將函數注冊到 defer 中,deferred 函數的參數值都是在注冊的時候進行求值的。
3.10 濫用 defer 可能會導致性能問題
濫用 defer 可能會導致性能問題,尤其是在一個 “大循環” 里。
defer 是在函數退出時調用的。如果在 for 語句的每個迭代都使用 defer 設置 deferred 函數,這些deferred 函數會壓入 runtime 實現的 defer 列表中。會占用內存資源,并且如果 for 的 loop 次數很多,這個消耗將很可觀。
3.11 defer 返回值被丟棄
defer 后邊調用的函數如果有返回值,則這個返回值將會被丟棄。
package mainimport "fmt"func demo() int {defer func() int {return 100}()return 8
}func main() {fmt.Println(demo()) // 8
}
上邊的示例代碼中,demo 函數的返回值是 8。defer 后邊調用的函數的返回值并不能作為 demo 函數的返回值。
3.12 defer 改變有名返回參數的值
defer 可以改變有名返回參數的值
這是由于在 Go 語言中,return 是函數的返回標志,并不代表執行結束。return 語句并不是原子操作,最先為返回值賦值,然后執行 defer 命令,最后才是真正意義上的 return 操作。
如果是有名返回值,返回值變量其實可視為是引用賦值,可以能被 defer 修改。而在匿名返回值時,給 ret 的值相當于拷貝賦值,defer 命令時不能直接修改。
有名返回值:
func demo() (i int)
上面函數簽名中的 i 就是有名返回值,如果 demo() 中定義了 defer 代碼塊,是可以改變返回值 i 的,函數返回語句 return i 可以簡寫為 return。
這里綜合了以上幾種情況,在下面這個例子里列舉了幾種情況,
package mainimport ("fmt"
)func main() {fmt.Println("=========================")fmt.Println("return:", fun1())fmt.Println("=========================")fmt.Println("return:", fun2())fmt.Println("=========================")fmt.Println("return:", fun3())fmt.Println("=========================")fmt.Println("return:", fun4())
}func fun1() (i int) {defer func() {i++fmt.Println("defer2:", i) // 打印結果為 defer2: 2}()// 規則二 defer執行順序為先進后出defer func() {i++fmt.Println("defer1:", i) // 打印結果為 defer1: 1}()// 規則三 defer可以改變有名返回參數的值return 0 //這里實際結果為2。如果是return 100呢
}func fun2() int {var i intdefer func() {i++fmt.Println("defer2:", i) // 打印結果為 defer2: 2}()defer func() {i++fmt.Println("defer1:", i) // 打印結果為 defer1: 1}()return i
}func fun3() (r int) {t := 5defer func() {t = t + 5fmt.Println(t)}()return t
}func fun4() int {i := 8// 規則一 defer后面的函數參數會被實時解析defer func(i int) {i = 99fmt.Println(i)}(i)i = 19return i
}
在上面 fun1() (i int) 有名返回值情況下,return 最終返回的實際值和期望的 return 0 有較大出入。
因為在上面 fun1() (i int) 中,如果 return 100 或 return 0 ,這樣的區別在于 i 的值實際上分別是 100 或 0。而在上面中,如果 return 100,則因為改變了有名返回值 i,而 defer 可以讀取有名返回值,所以返回值最終為 102,而 defer1 打印 101,defer 打印 102。因此一般直接寫為 return。
這點要注意,有時函數可能返回非我們希望的值,所以改為匿名返回也是一種辦法。具體請看下面輸出。
=========================
defer1: 1
defer2: 2
return: 2
=========================
defer1: 1
defer2: 2
return: 0
=========================
10
return: 5
=========================
99
return: 19
4. defer 釋放資源
處理業務或邏輯中涉及成對的操作是一件比較煩瑣的事情,比如打開和關閉文件、接收請求和回復請求、加鎖和解鎖等。在這些操作中,最容易忽略的就是在每個函數退出處正確地釋放和關閉資源。defer 語句正好是在函數退出時執行的語句,所以使用 defer 能非常方便地處理資源釋放問題。
4.1 使用延遲并發解鎖
在下面的例子中會在函數中并發使用 map ,為防止競態問題,使用 sync.Mutex 進行加鎖,參見下面代碼:
var (// 一個演示用的映射valueByKey = make(map[string]int)// map 默認不是并發安全的,準備一個 sync.Mutex 互斥量保護 map 的訪問。// 保證使用映射時的并發安全的互斥鎖valueByKeyGuard sync.Mutex
)// 根據鍵讀取值
func readValue(key string) int {// 對共享資源加鎖valueByKeyGuard.Lock()// 取值v := valueByKey[key]// 對共享資源解鎖valueByKeyGuard.Unlock()// 返回值return v
}
使用 defer 語句對上面的語句進行簡化,參考下面的代碼。
func readValue(key string) int {valueByKeyGuard.Lock()// defer后面的語句不會馬上調用, 而是延遲到函數結束時調用defer valueByKeyGuard.Unlock()return valueByKey[key]
}
4.2 使用延遲釋放文件句柄
文件的操作需要經過打開文件、獲取和操作文件資源、關閉資源幾個過程,如果在操作完畢后不關閉文件資源,進程將一直無法釋放文件資源。
在下面的例子中將實現根據文件名獲取文件大小的函數,函數中需要打開文件、獲取文件大小和關閉文件等操作,由于每一步系統操作都需要進行錯誤處理,而每一步處理都會造成一次可能的退出,因此就需要在退出時釋放資源,而我們需要密切關注在函數退出處正確地釋放文件資源,參考下面的代碼:
// 根據文件名查詢其大小
func fileSize(filename string) int64 {// 根據文件名打開文件, 返回文件句柄和錯誤f, err := os.Open(filename)// 如果打開時發生錯誤, 返回文件大小為0if err != nil {return 0}// 取文件狀態信息info, err := f.Stat()// 如果獲取信息時發生錯誤, 關閉文件并返回文件大小為0if err != nil {f.Close()return 0}// 取文件大小size := info.Size()// 關閉文件f.Close()// 返回文件大小return size
}
在上面的例子中,第 25 行是對文件的關閉操作,下面使用 defer 對代碼進行簡化,代碼如下:
func fileSize(filename string) int64 {f, err := os.Open(filename)if err != nil {return 0}// 延遲調用Close, 此時Close不會被調用defer f.Close() // defer 后的語句(f.Close())將會在函數返回前被調用,自動釋放資源。// 不能將這一句代碼放在第 4 行空行處,一旦文件打開錯誤,f 將為空,在延遲語句觸發時,將觸發宕機錯誤。info, err := f.Stat()if err != nil {// defer機制觸發, 調用Close關閉文件return 0}size := info.Size()// defer機制觸發, 調用Close關閉文件return size
}
總結
以上是默认站点為你收集整理的Go 学习笔记(17)— 函数(03)[defer 定义、defer 特点、defer 释放资源]的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 眼睛激光多少钱啊?
- 下一篇: Go 学习笔记(18)— 函数(04)[