Golang 中的 Defer
在Go語言中,defer
語句用于將一個函數調用推遲到外圍函數返回之后執行。它常用于確保某些操作在函數結束時一定會執行,例如資源釋放、文件關閉等。
基本語法
defer
語句的基本使用方法如下:
func main() {defer fmt.Println("World")fmt.Println("Hello")
}
輸出:
Hello
World
在上面的例子中,fmt.Println("World")
在defer
語句中,所以它會在main
函數結束時執行,而不是在定義它的地方立即執行。
多個defer
如果在一個函數中有多個defer
語句,它們的執行順序是后進先出(LIFO)的。也就是說,最后一個defer
語句會最先執行。
func main() {defer fmt.Println("First")defer fmt.Println("Second")defer fmt.Println("Third")fmt.Println("Hello")
}
輸出:
Hello
Third
Second
First
典型用例
1. 文件操作
在處理文件操作時,可以使用defer
確保文件關閉:
package mainimport ("fmt""os"
)func main() {file, err := os.Open("example.txt")if err != nil {fmt.Println(err)return}defer file.Close()// 讀取文件內容
}
2. 資源釋放
defer
還可以用于釋放其它類型的資源,例如網絡連接、數據庫連接等:
package mainimport ("database/sql"_ "github.com/go-sql-driver/mysql""log"
)func main() {db, err := sql.Open("mysql", "user:password@/dbname")if err != nil {log.Fatal(err)}defer db.Close()// 數據庫操作
}
3. 鎖的解鎖
在并發編程中,可以使用defer
確保鎖在臨界區操作完成后被釋放:
package mainimport ("fmt""sync"
)var mu sync.Mutexfunc main() {mu.Lock()defer mu.Unlock()// 臨界區操作fmt.Println("Critical section")
}
defer與匿名函數
有時候,我們需要在defer
中執行更復雜的操作,此時可以使用匿名函數:
package mainimport "fmt"func main() {defer func() {fmt.Println("Deferred call")}()fmt.Println("Main function")
}
輸出:
Main function
Deferred call
defer與返回值
defer
還可以用來修改返回值:
package mainimport "fmt"func test() (result int) {defer func() {result++}()return 0
}func main() {fmt.Println(test()) // 輸出1
}
在上面的例子中,defer
中的匿名函數會在test
函數返回之前執行,并修改result
的值。
defer 誤區
以下,我將以對比的形式展開對 defer
誤區的展示。(你可以先自己猜一下每段代碼的執行結果)
誤區一
func Defer() {i := 0defer func() {println(i)}()i = 1
}
在這個函數中,defer
語句延遲執行匿名函數,直到 Defer
函數即將返回。
這個匿名函數是一個閉包,它捕獲并引用了外部變量 i
。
因此,當 defer
延遲的匿名函數最終執行時,它打印的是閉包捕獲的變量 i
的當前值。
- 定義變量
i
并賦值為0
。 - 定義
defer
延遲執行的匿名函數,它捕獲變量i
。 - 將變量
i
修改為1
。 Defer
函數即將返回時,執行defer
延遲的匿名函數,打印捕獲的變量i
的當前值1
。
因此,Defer
函數的輸出是1
。
func DeferV1() {i := 0// 立即調用 defer 延遲的匿名函數,并將當前變量 i 的值傳遞給它的參數 i。defer func(i int) {println(i)}(i)i = 1
}
在這個函數中,defer
語句延遲執行的匿名函數有一個參數 i
,并且在調用時將當前的 i
值作為參數傳遞給匿名函數。
這意味著在 defer
聲明時,匿名函數參數 i
的值已經確定,并且與外部變量 i
無關。
- 定義變量
i
并賦值為0
。 - 定義
defer
延遲執行的匿名函數,并立即將當前變量i
(值為0
)傳遞給它的參數i
。 - 將外部變量
i
修改為1
。 DeferV1
函數即將返回時,執行defer
延遲的匿名函數,打印參數i
的值0
。
因此,DeferV1
函數的輸出是 0
。
誤區二
func DeferReturn() int {a := 0defer func() {a = 1}()return a
}
在這個函數中,變量 a
是一個局部變量。
當 return a
語句執行時,defer
語句的執行會在 return
語句之后立即發生,但在實際返回值被傳遞給調用者之前。
- 定義變量
a
并賦值為0
。 - 設置
defer
延遲執行的匿名函數,將變量a
修改為1
。 - 執行
return a
,此時返回值為0
。 defer
延遲的匿名函數執行,將變量a
修改為1
,但此時返回值已經確定為0
。
因此,DeferReturn
函數的返回值是 0
。
func DeferReturnV1() (a int) {// 全局可見 命名返回值 aa = 0defer func() {a = 1}()return
}
在這個函數中,變量 a
是一個命名返回值。
在 Go 語言中,當一個函數聲明了命名返回值時,該返回值變量會在函數開始時被隱式聲明,并且在整個函數體中都是可見的。
因此,當 return
語句執行時,返回的值是命名返回值變量 a
的當前值。
- 定義命名返回值變量
a
并隱式初始化為0
。 - 設置
defer
延遲執行的匿名函數,將變量a
修改為1
。 - 執行
return
語句,返回命名返回值變量a
。 - 在實際返回值被傳遞給調用者之前,執行
defer
延遲的匿名函數,將變量a
修改為1
。
因此,DeferReturnV1
函數的返回值是 1
。
誤區三
type MyStruct struct {name string
}func DeferReturnV2() *MyStruct {a := &MyStruct{name: "ypb",}defer func() {a.name = "zmz"}()return a
}
關鍵點:
- 指針修改
a
是一個指向 MyStruct
實例的指針。
在 defer
延遲的匿名函數中,修改的是 a
指向的對象的 name
字段,而不是指針本身。
這意味著,即使 return a
語句執行時,defer
語句會在實際返回之前執行,修改指針所指向的對象的內容。
- defer 的執行順序
defer
語句會在包含它的函數返回之前執行。
具體來說,defer
延遲的匿名函數在 return
語句設置返回值之后,實際返回之前執行。
因此,匿名函數在返回之前對指針所指向的對象的修改是有效的。
本例子執行順序:
- 創建
MyStruct
實例并賦值給a
,此時a.name = "ypb"
。 - 設置
defer
延遲執行的匿名函數,準備在函數返回之前執行。 - 執行
return a
,此時準備返回a
指向的對象。 - 在返回之前,執行
defer
延遲的匿名函數,將a.name
修改為"zmz"
。 - 返回
a
,此時a
指向的MyStruct
實例的name
字段已經被修改為"zmz"
。
defer 自測
只需要自己猜測一下代碼的輸出結果即可。
很多人誤以為在循環中使用 defer
會在每次迭代時執行推遲的操作。實際上,defer
是在函數返回時才執行的,因此在循環中多次使用 defer
會在函數結束時按照后進先出順序依次執行所有的 defer
語句。
測試一:
// DeferClosureLoop1 函數的輸出結果為 十個 10
func DeferClosureLoop1() {for i := 0; i < 10; i++ {i := idefer func() {println(i)}()}
}
測試二:
// DeferClosureLoop2 函數的輸出結果為 9 ~ 0
func DeferClosureLoop2() {for i := 0; i < 10; i++ {defer func(val int) {println(val)}(i)}
}
測試三:
// DeferClosureLoop3 與 DeferClosureLoop2 函數的輸出結果相同
func DeferClosureLoop3() {for i := 0; i < 10; i++ {j := idefer func() {println(j)}()}
}
總結
defer
在 Go 語言中用于推遲函數調用,直到外圍函數返回。
常見用法包括確保資源釋放(如文件關閉、解鎖)、在多層嵌套函數中統一處理異常等。
defer
語句按后進先出順序執行,支持匿名函數并可修改命名返回值。
需注意變量捕獲、循環中使用defer
導致堆積等誤區。正確使用defer
有助于代碼清晰與資源管理。