瞭解 Go 中的 defer
簡介
Go 有許多其他編程語言中常見的控制流關鍵字,如 if
、switch
、for
等。有一個關鍵詞在大多數其他編程語言中都沒有,那就是 defer
,雖然它不太常見,但你很快就會發現它在你的程序中是多麼有用。
defer
語句的主要用途之一是清理資源,如打開的文件、網絡連接和 數據庫句柄 [1]。當你的程序使用完這些資源後,關閉它們很重要,以避免耗盡程序的限制,並允許其他程序訪問這些資源。defer
通過保持關閉文件 / 資源的調用與打開調用保持一致,使我們的代碼更加簡潔,不易出錯。
在這篇文章中,我們將學習如何正確使用 defer
語句來清理資源,以及使用 defer
時常犯的幾個錯誤。
什麼是 defer
語句
defer
語句將 defer
關鍵字後面的 函數_(點擊查看哦)_調用添加到一個棧中。當該語句所在的函數返回時,將執行堆棧中所有的函數調用。由於這些調用位於堆棧上,因此將按照後進先出的順序進行調用。
讓我們看看 defer
是如何工作的,打印出一些文本:
main.go
package main
import "fmt"
func main() {
defer fmt.Println("Bye")
fmt.Println("Hi")
}
在 main
函數中,我們有兩條語句。第一條語句以 defer
關鍵字開始,後面是 print
語句,打印出 Bye
。下一行打印出 Hi
。
如果我們運行該程序,我們將看到以下輸出:
Hi
Bye
請注意,Hi
被首先打印出來。這是因爲以 defer
爲前綴的語句直到該函數結束前,都不會被調用。
讓我們再看看這個程序,這次我們將添加一些註釋來幫助說明正在發生的事情:
main.go
package main
import "fmt"
func main() {
// defer statement is executed, and places
// fmt.Println("Bye") on a list to be executed prior to the function returning
defer fmt.Println("Bye")
// The next line is executed immediately
fmt.Println("Hi")
// fmt.Println*("Bye") is now invoked, as we are at the end of the function scope
}
理解 defer
的關鍵是,當 defer
語句被執行時,延遲函數的參數被立即評估。當 defer
執行時,它把後面的語句放在一個列表中,在函數返回之前被調用。
雖然這段代碼說明了 defer
的運行順序,但這並不是編寫 Go 程序時的典型使用方式。我們更可能使用 defer
來清理資源,例如文件句柄。接下來讓我們看看如何做到這一點。
使用 defer
來清理資源
使用 defer
來清理資源在 Go 中是非常常見的。讓我們先看看一個將字符串寫入文件的程序,但沒有使用 defer
來處理資源清理的問題:
main.go
package main
import (
"io"
"log"
"os"
)
func main() {
if err := write("readme.txt", "This is a readme file"); err != nil {
log.Fatal("failed to write file:", err)
}
}
func write(fileName string, text string) error {
file, err := os.Create(fileName)
if err != nil {
return err
}
_, err = io.WriteString(file, text)
if err != nil {
return err
}
file.Close()
return nil
}
在這個程序中,有一個叫做 write
的函數,它將首先嚐試創建一個文件。如果它有錯誤,它將返回錯誤並退出函數。接下來,它試圖將字符串 This is a readme file
寫到指定文件中。如果它收到一個錯誤,它將返回錯誤並退出該函數。然後,該函數將嘗試關閉該文件並將資源釋放回系統。最後,該函數返回 nil
以表示該函數的執行沒有錯誤。
雖然這段代碼可以工作,但有一個細微的錯誤。如果對 io.WriteString
的調用失敗,該函數將在沒有關閉文件並將資源釋放回系統的情況下返回。
我們可以通過添加另一個 file.Close()
語句來解決這個問題,在沒有 defer
的語言中,你可能會這樣解決:
main.go
package main
import (
"io"
"log"
"os"
)
func main() {
if err := write("readme.txt", "This is a readme file"); err != nil {
log.Fatal("failed to write file:", err)
}
}
func write(fileName string, text string) error {
file, err := os.Create(fileName)
if err != nil {
return err
}
_, err = io.WriteString(file, text)
if err != nil {
file.Close()
return err
}
file.Close()
return nil
}
現在,即使調用 io.WriteString
失敗了,我們仍然會關閉該文件。雖然這是一個相對容易發現和修復的錯誤,但對於一個更復雜的函數來說,它可能會被遺漏。
我們可以使用 defer
語句來確保在執行過程中無論採取何種分支,我們都會調用 Close()
,而不是增加對 file.Close()
的第二次調用。
下面是使用 defer
關鍵字的版本:
main.go
package main
import (
"io"
"log"
"os"
)
func main() {
if err := write("readme.txt", "This is a readme file"); err != nil {
log.Fatal("failed to write file:", err)
}
}
func write(fileName string, text string) error {
file, err := os.Create(fileName)
if err != nil {
return err
}
defer file.Close()
_, err = io.WriteString(file, text)
if err != nil {
return err
}
return nil
}
這一次我們添加了這行代碼,defer file.Close()
。這告訴編譯器,它應該在退出函數 write
之前執行 file.Close
。
現在我們已經確保,即使我們在未來添加更多的代碼並創建另一個退出該函數的分支,我們也會一直清理並關閉該文件。
然而,我們通過添加 defer
引入了另一個錯誤。我們不再檢查可能從 Close
方法返回的潛在錯誤。這是因爲當我們使用 defer
時,沒有辦法將任何返回值傳回給我們的函數。
在 Go 中,在不影響程序行爲的情況下多次調用 Close()
被認爲是一種安全和公認的做法。如果 Close()
要返回一個錯誤,它將在第一次被調用時返回。這使得我們可以在函數的成功執行路徑中明確地調用它。
讓我們看看我們如何既能 defer
對 Close
的調用,又能在遇到錯誤時報告錯誤。
main.go
package main
import (
"io"
"log"
"os"
)
func main() {
if err := write("readme.txt", "This is a readme file"); err != nil {
log.Fatal("failed to write file:", err)
}
}
func write(fileName string, text string) error {
file, err := os.Create(fileName)
if err != nil {
return err
}
defer file.Close()
_, err = io.WriteString(file, text)
if err != nil {
return err
}
return file.Close()
}
這個程序中唯一的變化是最後一行,我們返回 file.Close()
。如果對 Close
的調用導致錯誤,現在將按照預期返回給調用函數。請記住,我們的 defer file.Close()
語句也將在 return
語句之後運行。這意味着 file.Close()
有可能被調用兩次。雖然這並不理想,但這是可以接受的做法,因爲它不應該對你的程序產生任何副作用。
然而,如果我們在函數的早期收到一個錯誤,例如當我們調用 WriteString
時,函數將返回該錯誤,並且也將嘗試調用 file.Close
,因爲它被推遲了。儘管 file.Close
也可能(而且很可能)返回一個錯誤,但這不再是我們關心的事情,因爲我們收到的錯誤更有可能告訴我們一開始就出了什麼問題。
到目前爲止,我們已經看到我們如何使用一個 defer
來確保我們正確地清理我們的資源。接下來我們將看到如何使用多個 defer
語句來清理多個資源。
多個 defer
語句
在一個函數中擁有多個 defer
語句是很正常的。讓我們創建一個只有 defer
語句的程序,看看當我們引入多個 defer
時,會發生什麼情況:
main.go
package main
import "fmt"
func main() {
defer fmt.Println("one")
defer fmt.Println("two")
defer fmt.Println("three")
}
如果我們運行該程序,我們將收到以下輸出結果:
three
two
one
注意,順序與我們調用 defer
語句的順序相反。這是因爲每個被調用的延遲語句都是堆疊在前一個語句之上的,然後在函數退出範圍時反向調用(後進先出)。
在一個函數中,你可以根據需要有儘可能多的 defer
調用,但重要的是要記住它們都將以相反的順序被調用。
現在我們瞭解了多個延遲的執行順序,讓我們看看如何使用多個延遲來清理多個資源。我們將創建一個程序,打開一個文件,向其寫入內容,然後再次打開,將內容複製到另一個文件。
main.go
package main
import (
"fmt"
"io"
"log"
"os"
)
func main() {
if err := write("sample.txt", "This file contains some sample text."); err != nil {
log.Fatal("failed to create file")
}
if err := fileCopy("sample.txt", "sample-copy.txt"); err != nil {
log.Fatal("failed to copy file: %s")
}
}
func write(fileName string, text string) error {
file, err := os.Create(fileName)
if err != nil {
return err
}
defer file.Close()
_, err = io.WriteString(file, text)
if err != nil {
return err
}
return file.Close()
}
func fileCopy(source string, destination string) error {
src, err := os.Open(source)
if err != nil {
return err
}
defer src.Close()
dst, err := os.Create(destination)
if err != nil {
return err
}
defer dst.Close()
n, err := io.Copy(dst, src)
if err != nil {
return err
}
fmt.Printf("Copied %d bytes from %s to %s\n", n, source, destination)
if err := src.Close(); err != nil {
return err
}
return dst.Close()
}
我們添加了一個新的函數,叫做 fileCopy
。在這個函數中,我們首先打開我們要複製的源文件。我們檢查我們是否收到了一個打開文件的錯誤。如果是的話,我們 return
錯誤並退出該函數。否則,我們 defer
關閉我們剛剛打開的源文件。
接下來我們創建目標文件。再次,我們檢查我們是否收到了創建文件的錯誤。如果是的話,我們 return
該錯誤並退出該函數。否則,我們也 defer
目標文件的 Close()
。我們現在有兩個 defer
函數,當函數退出其作用域時將被調用。
現在我們已經打開了兩個文件,我們將Copy()
數據從源文件到目標文件。如果成功的話,我們將嘗試關閉兩個文件。如果我們在試圖關閉任何一個文件時收到錯誤,我們將 return
錯誤並退出函數作用域。
注意,我們爲每個文件明確地調用 Close()
,儘管 defer
也將調用 Close()
。這是爲了確保如果關閉文件時出現錯誤,我們會報告這個錯誤。這也確保瞭如果因爲任何原因函數提前退出,例如我們在兩個文件之間複製失敗,每個文件仍將嘗試從延遲調用中正確關閉。
總結
在這篇文章中,我們瞭解了 defer
語句,以及如何使用它來確保我們在程序中正確清理系統資源。正確地清理系統資源將使你的程序使用更少的內存,表現更好。要了解更多關於 defer
的使用,請閱讀 處理恐慌_(點擊查看)_的文章,或者探索我們整個 《如何在 Go 中編碼系列》[2]。
相關鏈接:
[1] https://en.wikipedia.org/wiki/Handle_(computing)
[2] https://gocn.github.io/How-To-Code-in-Go/
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/Fuc4557t3OWvJ3S2oHi1cA