首頁  >  文章  >  後端開發  >  分享優質筆記:go閉包的使用學習

分享優質筆記:go閉包的使用學習

藏色散人
藏色散人轉載
2021-08-26 16:36:412193瀏覽

下面由go語言教學專欄跟大家介紹關於go 閉包的學習使用,希望對需要的朋友有幫助!

go閉包

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

匿名函數

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

package mainimport (
    "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 functionhello golang, I am Anonymous function

透過建立一個傳回函數的函數來使用匿名函數。

package mainimport (
    "fmt")func sayhello(s string) func() {
    return func() {
        fmt.Println("hello", s)
    }}func main() {
    sayhello := sayhello("golang")
    sayhello()}

結果輸出:

hello golang

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

閉包的本質

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

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

package mainimport (
    "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 mainimport (
    "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 mainimport (
  "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 mainimport (
  "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 mainimport (
  "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 mainimport (
    "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 mainimport (
    "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过程,否则会导致当前进程的异常退出,而这并不是我们所期望的。

                                      

以上是分享優質筆記:go閉包的使用學習的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:learnku.com。如有侵權,請聯絡admin@php.cn刪除