- 原文地址:Part 29: Defer
- 原文作者:Naveen R
- 譯者:咔嘰咔嘰 轉載請註明出處。
什麼是 Defer
在存在defer
語句的函式返回之前,會執行defer
的呼叫。定義可能看起來有點難懂,但通過示例來理解它非常簡單。
package 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)
}
複製程式碼
Run in playgroud
以上是一個簡單的程式,用於查詢給定切片最大的數。largest
函式將int
切片作為引數,並輸出該切片的最大數。largest
函式的第一行包含語句defer finished()
。這意味著在largest
函式返回之前將呼叫finished
函式。執行此程式,可以看到以下輸出。
Started finding largest
Largest number in [78 109 2 563 300] is 563
Finished finding largest
複製程式碼
largest
函式開始執行並列印上述輸出的前兩行。在它返回之前,defer
函式完成執行,並列印Finished finding largest
:)
defer
一個方法
defer
不僅限於函式。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 ")
}
複製程式碼
在上面的程式中,我們defer
了一個方法的呼叫,其餘的程式碼是不難懂的。該程式輸出,
Welcome John Smith
複製程式碼
defer
的引數作用域
defer
的函式的引數是在執行defer
語句時傳入的,在實際函式呼叫的時候defer
函式的引數還是當初傳入的引數。
來一個例子,
package 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,defer
語句實在第 12 行。a
是defer
函式printA
的引數。在第 13 行我們將a
的值更改為 10。該程式的輸出,
value of a before deferred function call 10
value of a in deferred function 5
複製程式碼
從上面的輸出可以看到,儘管在執行defer
語句之後a
的值變為 10,但實際的defer
函式呼叫printA(a)
仍然列印 5。
多個defer
函式的呼叫順序
當一個函式有多個defer
呼叫時,它們會被新增到棧中並以後進先出(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
迴圈迭代字串並呼叫defer fmt.Printf("%c", v)輸出字元。這些
defer`呼叫將被新增到棧中並以後進先出的順序執行,因此字串將以相反的順序列印。該程式將輸出,
Orignal String: Naveen
Reversed String: neevaN
複製程式碼
defer
的實際用法
到目前為止我們看到的程式碼示例沒有顯示defer
的實際用法。在本節中,我們將研究defer
的一些實際用途。
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")
}
複製程式碼
Run in playground
在上面的程式中,我們在第 8 行建立了一個rect
結構,第 13 行給rect
結構加上了area
方法用於計算矩形的面積。此方法檢查矩形的長度和寬度是否小於 0。如果是這樣,它會列印相應的訊息,否則會列印矩形的面積。
main
函式建立了 3 個型別為rect
的變數r1
,r2
和r3
,將它們新增到rects
切片中。然後使用for range
迴圈迭代該切片,並將area
方法併發執行。 WaitGroup wg
用於保證所有Goroutines
執行完畢。WaitGroup
作為引數傳遞給area
方法,並在area
方法中呼叫wg.Done
,主要通知main
該Goroutine
已完成其工作。如果您仔細觀察,可以看到這些呼叫恰好在area
方法返回之前發生。無論程式碼採用哪個條件分支執行,都應在方法返回之前呼叫wg.Done
,因此可以通過defer
來解決這種場景。
來用defer
重寫上面的程式吧。
在下面的程式中,我們刪除了上面程式中的 3 個wg.Done
,並將其替換為defer wg.Done()
,這將使程式碼更簡潔易懂。
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")
}
複製程式碼
rect {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
方法新增另一個處理分支。如果沒有defer
對wg.Done
的呼叫,我們必須小心確保在這個新的處理分支中呼叫wg.Done
。但由於對wg.Done
的呼叫用了defer
,我們再也不用擔心這種情況了。相似的應用場景應該還有很多,比如開啟檔案的關閉等等。但是需要注意的是,大量的使用defer
函式會導致程式執行效率變低。