go 閉包學習筆記

ramsey發表於2021-08-14

go閉包

在 Golang 中,閉包是一個可以引用其作用域之外變數的函式。
換句話說,閉包是一個內部函式,它可以訪問建立它的範圍內的變數。即使外部函式完成執行並且作用域被破壞,依然可以訪問。
在深入研究閉包之前,需要了解什麼是匿名函式。

匿名函式

顧名思義,匿名函式就是沒有名字的函式。
舉個例子,我們建立一個一般函式和匿名函式

package main

import (
    "fmt"
)

func sayhi1() { // 一般函式
    fmt.Println("hello golang, I am Regular function")
}

func main() {
    sayhi1()
    sayhi2 := func() { //匿名函式
        fmt.Println("hello golang, I am Anonymous function")
    }
    sayhi2()
}

結果輸出:

hello golang, I am Regular function
hello golang, I am Anonymous function

透過建立一個返回函式的函式來使用匿名函式。

package main

import (
    "fmt"
)

func sayhello(s string) func() {
    return func() {
        fmt.Println("hello", s)
    }
}

func main() {
    sayhello := sayhello("golang")
    sayhello()
}

結果輸出:

hello golang

常規函式和匿名函式唯一區別是匿名函式不是在包級別宣告的,它們被動態地宣告,通常要麼被使用,要麼被遺忘,要麼被分配給一個變數供以後使用。

閉包的本質

閉包是包含自由變數的程式碼塊,這些變數不在這個程式碼塊內或者任何全域性上下文中定義,而是在定義程式碼塊的環境中定義。由於自由變數包含在程式碼塊中,所以只要閉包還被使用,那麼這些自由變數以及它們引用的物件就不會被釋放,要執行的程式碼為自由變數提供繫結的計算環境。
閉包的價值在於可以作為函式物件或者匿名函式,對於型別系統而言,這意味著不僅要表示資料還要表示程式碼。支援閉包的多數語言都將函式作為第一級物件,就是說這些函式可以儲存到變數中作為引數傳遞給其他函式,最重要的是能夠被函式動態建立和返回。

Golang中的閉包同樣也會引用到函式外的變數,閉包的實現確保只要閉包還被使用,那麼被閉包引用的變數會一直存在。從形式上看,匿名函式都是閉包。
我們來看個閉包例子:

package main

import (
    "fmt"
)

func caller() func() int {
    callerd := 1
    sum := 0
    return func() int {
        sum += callerd
        return sum
    }

}

func main() {
    next := caller()
    fmt.Println(next())
    fmt.Println(next())
    fmt.Println(next())
}

結果輸出:

1
2
3

該例子中called 和sum 是自由變數,caller函式返回的匿名函式為自由變數提供了計算環境,匿名函式和自由變數組成的程式碼塊其實就是閉包。在閉包函式中,只有匿名函式才能訪問自由變數called和sum,而無法透過其他途徑訪問,因此閉包自由變數的安全性。
按照命令式語言的規則,caller函式只是返回了匿名函式的地址,但在執行匿名函式時將會由於在其作用域內找不到sum和called變數而出錯。而在函式式語言中,當內嵌函式體內引用到體外的變數時,將會把定義時涉及到的引用環境和函式體打包成一個整體(閉包)返回。閉包的使用和正常的函式呼叫沒有區別。

現在我們給出引用環境的定義:在程式執行中的某個點所有處於活躍狀態的約束所組成的集合,其中的約束指的是一個變數的名字和其所代表的物件之間的聯絡。
所以我們說:閉包=函式+引用環境

其實我們可以將閉包函式看成一個類(C++),一個閉包函式呼叫就是例項化一個物件,閉包的自由變數就是類的成員變數,閉包函式的引數就是類的函式物件的引數。在該例子中,next可以看作是例項化的一個物件,next()可以看做是物件函式呼叫的返回值。
這讓我們想起了一句名言:物件是附有行為的資料,而閉包是附有資料的行為

閉包使用的一些例子

  • 利用閉包實現資料隔離
    假設你想建立一個函式,該函式可以訪問即使在函式退出後仍然存在的資料。舉個例子,如果你想統計函式被呼叫的次數,但不希望其他任何人訪問該資料(這樣他們就不會意外更改它),你就可以用閉包來實現它:
package main

import (
    "fmt"
)

func caller() func() int {
    callerd := 0
    return func() int {
        callerd++
        return callerd
    }

}

func main() {
    next := caller()
    fmt.Println(next())
    fmt.Println(next())
    fmt.Println(next())
}

結果輸出:

1
2
3
  • 利用閉包包裝函式和建立中介軟體
    Go 中的函式是一等公民。這意味著您不僅可以動態建立匿名函式,還可以將函式作為引數傳遞給函式。例如,在建立 Web 伺服器時,通常會提供一個功能來處理Web 請求到特定的路由。
package main

import (
  "fmt"
  "net/http"
)

func main() {
  http.HandleFunc("/hello", hello)
  http.ListenAndServe(":3000", nil)
}

func hello(w http.ResponseWriter, r *http.Request) {
  fmt.Fprintln(w, "<h1>Hello!</h1>")
}

在上面例子中,函式 hello() 被傳遞給 http.HandleFunc() 函式,並在該路由匹配時呼叫。
雖然這段程式碼不需要閉包,但如果我們想用更多邏輯包裝我們的處理程式,閉包是非常有用的。一個完美的例子是我們可以透過建立中介軟體來在我們處理程式執行之前或之後做一些其它的工作。
什麼是中介軟體?
中介軟體基本上是可重用功能的一個奇特術語,它可以在設計用於處理 Web 請求的程式碼之前和之後執行程式碼。在 Go 中,這些通常是透過閉包來實現的,但在不同的程式語言中,可以透過其他方式來實現。
在編寫 Web 應用程式時使用中介軟體很常見,而且它們不僅可用於計時器(您將在下面看到一個示例)。例如,中介軟體可用於編寫程式碼驗證使用者是否登入過一次,然後將其應用到你的所有會員專頁。
讓我們看看一個簡單的計時器中介軟體在 Go 中是如何工作的。

package main

import (
  "fmt"
  "net/http"
  "time"
)

func main() {
  http.HandleFunc("/hello", timed(hello))
  http.ListenAndServe(":3000", nil)
}

func timed(f func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
  return func(w http.ResponseWriter, r *http.Request) {
    start := time.Now()
    f(w, r)
    end := time.Now()
    fmt.Println("The request took", end.Sub(start))
  }
}

func hello(w http.ResponseWriter, r *http.Request) {
  fmt.Fprintln(w, "<h1>Hello!</h1>")
}

timed() 函式接受一個可以用作處理函式的函式,並返回一個相同型別的函式,但返回的函式與傳遞它的函式不同。返回的閉包記錄當前時間,呼叫原始函式,最後記錄結束時間並列印出請求的持續時間。同時對我們的處理程式函式內部實際發生的事情是不可知的。
現在我們要做的就是將我們的處理程式包裝在 timed(handler) 中並將閉包傳遞給 http.HandleFunc() 函式呼叫。

  • 利用閉包使用 sort 二分搜尋
    使用標準庫中的包也經常需要閉包,例如 sort 包。這個包為我們提供了大量有用的功能和程式碼,用於排序和搜尋排序列表。例如,如果您想對一個整數切片進行排序,然後在切片中搜尋數字 7,您可以像這樣使用 sort 包。
package main

import (
  "fmt"
  "sort"
)

func main() {
  numbers := []int{1, 11, -5, 7, 2, 0, 12}
  sort.Ints(numbers)
  fmt.Println("Sorted:", numbers)
  index := sort.SearchInts(numbers, 7)
  fmt.Println("7 is at index:", index)
}

結果輸出:

Sorted: [-5 0 1 2 7 11 12]
7 is at index: 4

如果要搜尋的每個元素都是自定義型別的切片會發生什麼?或者,如果您想找到第一個等於或大於 7 的數字的索引,而不僅僅是 7 的第一個索引?
為此,您可以使用 sort.Search() 函式,並且您需要傳入一個閉包,該閉包可用於確定特定索引處的數字是否符合您的條件。
sort.Search() is a binary search
sort.Search 函式執行二分搜尋,因此它需要一個閉包,該閉包在滿足您的條件之前對任何索引返回 false,在滿足後返回 true。
讓我們使用上面描述的示例來看看它的實際效果;我們將搜尋列表中第一個大於或等於 7 的數字的索引。

package main

import (
    "fmt"
    "sort"
)

func main() {
    numbers := []int{1, 11, -5, 8, 2, 0, 12}
    sort.Ints(numbers)
    fmt.Println("Sorted:", numbers)

    index := sort.Search(len(numbers), func(i int) bool {
        return numbers[i] >= 7
    })
    fmt.Println("The first number >= 7 is at index:", index)
    fmt.Println("The first number >= 7 is:", numbers[index])
}

結果輸出:

Sorted: [-5 0 1 2 8 11 12]
The first number >= 7 is at index: 4
The first number >= 7 is: 8

在這個例子中,我們的閉包是作為第二個引數傳遞給 sort.Search() 的簡單函式。
這個閉包訪問數字切片,即使它從未被傳入,併為任何大於或等於 7 的數字返回 true。透過這樣做,它允許 sort.Search() 工作而無需瞭解什麼您使用的基礎資料型別是什麼,或者您試圖滿足什麼條件。它只需要知道特定索引處的值是否符合您的標準。

  • 用閉包+defer進行處理異常
package main

import (
    "fmt"
)

func handle() {
    defer func() {
        err := recover()
        if err != nil {
            fmt.Println("some except had happend:", err)
        }
    }()
    var a *int = nil
    *a = 100
}

func main() {
    handle()
}

結果輸出:

some except had happend: runtime error: invalid memory address or nil pointer dereference

recover函式用於終止錯誤處理流程。一般情況下,recover應該在一個使用defer關鍵字的函式中執行以有效擷取錯誤處理流程。如果沒有在發生異常的goroutine中明確呼叫恢復過程(呼叫recover函式),會導致該goroutine所屬的程式列印異常資訊後直接退出
對於第三方庫的呼叫,在不清楚是否有panic的情況下,最好在適配層統一加上recover過程,否則會導致當前程式的異常退出,而這並不是我們所期望的。

本作品採用《CC 協議》,轉載必須註明作者和本文連結
拉姆塞

相關文章