Go Defer
29. Defer
什麼是 defer?
defer
語句的用途是:含有 defer
語句的函式,會在該函式將要返回之前,呼叫另一個函式。這個定義可能看起來很複雜,我們通過一個示例就很容易明白了。
示例
Copypackage main
import (
"fmt"
)
func finished() {
fmt.Println("Finished finding largest")
}
func largest(nums []int) {
defer finished()
fmt.Println("Started finding largest" )
max := nums[0]
for _, v := range nums {
if v > max {
max = v
}
}
fmt.Println("Largest number in", nums, "is", max)
}
func main() {
nums := []int{78, 109, 2, 563, 300}
largest(nums)
}
上面的程式很簡單,就是找出一個給定切片的最大值。largest
函式接收一個 int 型別的切片作為引數,然後打印出該切片中的最大值。largest
defer finished()
。這表示在 finished()
函式將要返回之前,會呼叫 finished()
函式。執行該程式,你會看到有如下輸出:
Copy
Started finding largest
Largest number in [78 109 2 563 300] is 563
Finished finding largest
largest
函式開始執行後,會列印上面的兩行輸出。而就在 largest
將要返回的時候,又呼叫了我們的延遲函式(Deferred Function),打印出 Finished finding largest
的文字。
延遲方法
defer
不僅限於[函式]的呼叫,呼叫[方法]也是合法的。我們寫一個小程式來測試吧。
package main
import (
"fmt"
)
type person struct {
firstName string
lastName string
}
func (p person) fullName() {
fmt.Printf("%s %s",p.firstName,p.lastName)
}
func main() {
p := person {
firstName: "John",
lastName: "Smith",
}
defer p.fullName()
fmt.Printf("Welcome ")
}
在上面的例子中,我們在第 22 行延遲了一個方法呼叫。而其他的程式碼很直觀,這裡不再解釋。該程式輸出:
CopyWelcome John Smith
實參取值(Arguments Evaluation)
在 Go 語言中,並非在呼叫延遲函式的時候才確定實參,而是當執行 defer
語句的時候,就會對延遲函式的實參進行求值。
通過一個例子就能夠理解了。
Copypackage main
import (
"fmt"
)
func printA(a int) {
fmt.Println("value of a in deferred function", a)
}
func main() {
a := 5
defer printA(a)
a = 10
fmt.Println("value of a before deferred function call", a)
}
在上面的程式裡的第 11 行,a
的初始值為 5。在第 12 行執行 defer
語句的時候,由於 a
等於 5,因此延遲函式 printA
的實參也等於 5。接著我們在第 13 行將 a
的值修改為 10。下一行會打印出 a
的值。該程式輸出:
value of a before deferred function call 10
value of a in deferred function 5
從上面的輸出,我們可以看出,在呼叫了 defer
語句後,雖然我們將 a
修改為 10,但呼叫延遲函式 printA(a)
後,仍然列印的是 5。
defer 棧
當一個函式內多次呼叫 defer
時,Go 會把 defer
呼叫放入到一個棧中,隨後按照後進先出(Last In First Out, LIFO)的順序執行。
我們下面編寫一個小程式,使用 defer
棧,將一個字串逆序列印。
package main
import (
"fmt"
)
func main() {
name := "Naveen"
fmt.Printf("Orignal String: %s\n", string(name))
fmt.Printf("Reversed String: ")
for _, v := range []rune(name) {
defer fmt.Printf("%c", v)
}
}
在上述程式中的第 11 行,for range
迴圈會遍歷一個字串,並在第 12 行呼叫了 defer fmt.Printf("%c", v)
。這些延遲呼叫會新增到一個棧中,按照後進先出的順序執行,因此,該字串會逆序打印出來。該程式會輸出:
Orignal String: Naveen
Reversed String: neevaN
defer 的實際應用
目前為止,我們看到的程式碼示例,都沒有體現出 defer
的實際用途。本節我們會看看 defer
的實際應用。
當一個函式應該在與當前程式碼流(Code Flow)無關的環境下呼叫時,可以使用 defer
。我們通過一個用到了 [WaitGroup
] 程式碼示例來理解這句話的含義。我們首先會寫一個沒有使用 defer
的程式,然後我們會用 defer
來修改,看到 defer
帶來的好處。
package main
import (
"fmt"
"sync"
)
type rect struct {
length int
width int
}
func (r rect) area(wg *sync.WaitGroup) {
if r.length < 0 {
fmt.Printf("rect %v's length should be greater than zero\n", r)
wg.Done()
return
}
if r.width < 0 {
fmt.Printf("rect %v's width should be greater than zero\n", r)
wg.Done()
return
}
area := r.length * r.width
fmt.Printf("rect %v's area %d\n", r, area)
wg.Done()
}
func main() {
var wg sync.WaitGroup
r1 := rect{-67, 89}
r2 := rect{5, -67}
r3 := rect{8, 9}
rects := []rect{r1, r2, r3}
for _, v := range rects {
wg.Add(1)
go v.area(&wg)
}
wg.Wait()
fmt.Println("All go routines finished executing")
}
在上面的程式裡,我們在第 8 行建立了 rect
結構體,並在第 13 行建立了 rect
的方法 area
,計算出矩形的面積。area
檢查了矩形的長寬是否小於零。如果矩形的長寬小於零,它會打印出對應的提示資訊,而如果大於零,它會打印出矩形的面積。
main
函式建立了 3 個 rect
型別的變數:r1
、r2
和 r3
。在第 34 行,我們把這 3 個變數新增到了 rects
切片裡。該切片接著使用 for range
迴圈遍歷,把 area
方法作為一個併發的 Go 協程進行呼叫(第 37 行)。我們用 WaitGroup wg
來確保 main
函式在其他協程執行完畢之後,才會結束執行。WaitGroup
作為引數傳遞給 area
方法後,在第 16 行、第 21 行和第 26 行通知 main
函式,表示現在協程已經完成所有任務。如果你仔細觀察,會發現 wg.Done() 只在 area 函式返回的時候才會呼叫。wg.Done() 應該在 area 將要返回之前呼叫,並且與程式碼流的路徑(Path)無關,因此我們可以只調用一次 defer,來有效地替換掉 wg.Done() 的多次呼叫。
我們來用 defer
來重寫上面的程式碼。
在下面的程式碼中,我們移除了原先程式中的 3 個 wg.Done
的呼叫,而是用一個單獨的 defer wg.Done()
來取代它(第 14 行)。這使得我們的程式碼更加簡潔易懂。
package main
import (
"fmt"
"sync"
)
type rect struct {
length int
width int
}
func (r rect) area(wg *sync.WaitGroup) {
defer wg.Done()
if r.length < 0 {
fmt.Printf("rect %v's length should be greater than zero\n", r)
return
}
if r.width < 0 {
fmt.Printf("rect %v's width should be greater than zero\n", r)
return
}
area := r.length * r.width
fmt.Printf("rect %v's area %d\n", r, area)
}
func main() {
var wg sync.WaitGroup
r1 := rect{-67, 89}
r2 := rect{5, -67}
r3 := rect{8, 9}
rects := []rect{r1, r2, r3}
for _, v := range rects {
wg.Add(1)
go v.area(&wg)
}
wg.Wait()
fmt.Println("All go routines finished executing")
}
該程式會輸出:
Copyrect {8 9}'s area 72
rect {-67 89}'s length should be greater than zero
rect {5 -67}'s width should be greater than zero
All go routines finished executing
在上面的程式中,使用 defer
還有一個好處。假設我們使用 if
條件語句,又給 area
方法添加了一條返回路徑(Return Path)。如果沒有使用 defer
來呼叫 wg.Done()
,我們就得很小心了,確保在這條新添的返回路徑裡呼叫了 wg.Done()
。由於現在我們延遲呼叫了 wg.Done()
,因此無需再為這條新的返回路徑新增 wg.Done()
了。