go 中 defer 的一個隱藏功能

KevinYan發表於2019-12-11

在開始使用Go進行編碼時,Defer是要關注的一個很重要的特性。它非常簡單:在任何函式中,給其他函式的呼叫加上字首 defer以確保該函式在外部函式退出之前立即執行,即使外部函式出現異常被中斷,該延遲函式也將執行。

但是,你還可以使用defer在任何函式開始後和結束前執行配對的程式碼。這個隱藏的功能在網上的教程和書籍中很少提到。要使用此功能,需要建立一個函式並使它本身返回另一個函式,返回的函式將作為真正的延遲函式。在 defer 語句呼叫父函式後在其上新增額外的括號來延遲執行返回的子函式如下所示:

func main() {
    defer greet()() 
    fmt.Println("Some code here...")
}

func greet() func() {
    fmt.Println("Hello!")
    return func() { fmt.Println("Bye!") } // this will be deferred
}

輸出以下內容:

Hello!
Some code here...
Bye!

父函式返回的函式將是實際的延遲函式。父函式中的其他程式碼將在函式開始時(由 defer 語句放置的位置決定)立即執行。

這為開發者提供了什麼能力?因為在函式內定義的匿名函式可以訪問完整的詞法環境(lexical environment),這意味著在函式中定義的內部函式可以引用該函式的變數。在下一個示例中看到的,引數變數在measure函式第一次執行和其延遲執行的子函式內都能訪問到:

func main() {
    example()
    otherExample()
}

func example(){
    defer measure("example")()
    fmt.Println("Some code here")
}

func otherExample(){
    defer measure("otherExample")()
    fmt.Println("Some other code here")
}

func measure(name string) func() {
    start := time.Now()
    fmt.Printf("Starting function %s\n", name)
    return func(){ fmt.Printf("Exiting function %s after %s\n", name, time.Since(start)) }
}

輸出以下內容:

Starting example
Some code here
Exiting example after 0s
Starting otherExample
Some other code here
Exiting otherExample after 0s

此外函式命名的返回值也是函式內的區域性變數,所以上面例子中的measure函式如果接收命名返回值作為引數的話,那麼命名返回值在延遲執行的函式中訪問到,這樣就能將measure函式改造成記錄入參和返回值的工具函式。

下面的示例是引用《go 語言程式設計》中的程式碼段:

func bigSlowOperation() {
    defer trace("bigSlowOperation")() // don't forget the extra parentheses
    // ...lots of work…
    time.Sleep(10 * time.Second) // simulate slow
    operation by sleeping
}
func trace(msg string) func() {
    start := time.Now()
    log.Printf("enter %s", msg)
    return func() { 
        log.Printf("exit %s (%s)", msg,time.Since(start)) 
    }
}

可以想象,將程式碼延遲在函式的入口和出口使用是非常有用的功能,尤其是在除錯程式碼的時候。

本作品採用《CC 協議》,轉載必須註明作者和本文連結
公眾號:網管叨bi叨 | Golang、Laravel、Docker、K8s等學習經驗分享

相關文章