日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 默认站点!

默认站点

當前位置: 首頁 >

Go 学习笔记(17)— 函数(03)[defer 定义、defer 特点、defer 释放资源]

發布時間:2023/11/27 35 豆豆
默认站点 收集整理的這篇文章主要介紹了 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 函數,每當 deferfmt.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 列表中。會占用內存資源,并且如果 forloop 次數很多,這個消耗將很可觀。

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 100return 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 释放资源]的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得默认站点網站內容還不錯,歡迎將默认站点推薦給好友。