go語言閉包問題

1.什麼是閉包

官方的講,閉包是指能夠包含自由變量(未綁定到特定對象)變量的代碼塊;這些變量不是在這個代碼塊內活在任何全局上下文中定義的,而是在定義代碼塊的環境中定義的(局部變量), 當在這個代碼塊所在環境的外部調用該代碼塊時,代碼塊和它所引用的自由變量構成閉包。以下圖所示。
圖片.png

2.閉包的做用

從圖1很容易看出,自由變量是局部變量,外部沒法訪問,但和它同屬一個局部環境的代碼塊卻能夠訪問,所以,咱們能夠經過代碼塊間接地訪問內部的自由變量。若是代碼塊理解起來有點兒抽象,咱們下面看一個具體的例子:
package main
    import "fmt"

    func A() func(){
        // n是自由變量
        var n int = 2019
        // 匿名函數至關於圖1中的「代碼塊」
        return func() {
            n += 1
            fmt.Println(n)
        }
    }
    // main()在外部環境中
    func main() {
        myFunc := A()
        // 外部環境調用代碼塊
        myFunc()
        myFunc()
    }
在上面的代碼中,函數A內部有一個局部變量n, 爲了能在外部環境中訪問n,咱們讓函數A返回了一個匿名函數,這個匿名函數有對n的訪問權限,咱們在main函數調用函數A返回的匿名函數,能夠修改並打印出n的值,代碼編譯運行的結果以下:
2020
    2021

    Process finished with exit code 0
能夠看出,這個A內部的匿名函數就是函數A暴露給外部訪問其內部變量n的一個「接口」,經過這個接口,調用者能夠實如今全局環境中訪問局部變量。

3.閉包的好處

  • 咱們再來觀察上面代碼的運行結果,發如今main函數中兩次調用A的匿名函數,結果竟然不一樣。
  • 通常狀況下,在函數func執行完後,函數以前申請的棧空間會被釋放,函數中的局部變量也會被銷燬,下次再出現函數調用時,從新申請棧空間,而且從新初始化函數內部的局部變量。
  • 可是在閉包調用的狀況下,狀況會變得不一樣,因爲在main函數調用了A返回的匿名函數,至關於myFunc = func() {n += 1; fmt.Println(n);},而且匿名函數內部引用着A裏的局部變量n,因此致使在main函數一次函數調用結束後,n沒法被銷燬,由於它此時整被main函數中的myFunc引用着,它的生命週期和myFunc的生命週期相同,在main函數執行完畢時纔會被釋放。
  • 所以,當使用閉包方式訪問某個局部變量時,該局部變量會常駐內存,訪問速度會特別快,而且咱們始終操做的都是這個變量的自己,而不是其副本,修改時也特別方便。閉包調用的大體過程以下圖所示(不是很準確,會意便可!)
圖片.png
  • 爲了更好地理解閉包,咱們再看一個例子。編程

    • 編寫一個函數makeSuffix(suffix string)能夠接收一個文件後綴名(好比.jpg),並返回一個匿名函數;
    • 調用閉包,能夠傳入一個文件名,若是該文件名沒有指定的後綴(好比.jpg),則返回文件名.jpg, 若是已經有.jpg後綴,則返回原文件名。
    • 代碼以下所示:
package main

        import (
            "fmt"
            "strings"
        )

        // 處理文件名
        func makeSuffix(suffix string) func (string) string {
            return func(name string) string {
                // 匿名函數綁定的局部變量是外部函數形參suffix
                if !strings.HasSuffix(name, suffix) {
                    // 若是文件名沒有指定的後綴名,則給它加上指定的後綴名
                    name += suffix
                }
                return name
            }
        }

        func main() {
            // 1.指定文件後綴名爲.jpg
            f := makeSuffix(".jpg")
            // 2.建立文件名
            fileName1 := "flower"
            fileName2 := "flower.jpg"
            // 3.調用f
            res1 := f(fileName1)
            res2 := f(fileName2)
            // 4.打印閉包調用處理後的結果
            fmt.Println("res1=", res1)
            fmt.Println("res2=", res2)
        }
  • 編譯運行後的結果以下:
res1= flower.jpg
        res2= flower.jpg

        Process finished with exit code 0
  • 從結果能夠看出,原來不是之後綴.jpg結尾的文件名被加上了.jpg,以.jpg結尾的文件名沒有變化。
  • 在上述代碼中,返回的匿名函數和makeSuffix(suffix string)的suffix變量組成了閉包關係,由於返回的匿名函數引用到了這個變量。
  • 咱們體會一下閉包的好處,若是使用傳統的方法,也能夠很容易實現這個功能,可是傳統須要每次都傳入後綴名,好比.jpg, 而閉包是由於初次調用時閉包綁定的變量已常常駐內存,因此傳入一次就能夠反覆使用。

4.閉包的總結

(1) 一般是經過嵌套的匿名函數的形式實現的;
(2) 匿名函數內部引用了外部函數的參數或變量;
(3) 被引用的參數和變量的生命週期和外部調用者的生命週期相同。

5.防止閉包的誤用

請看下面的代碼:
package main

    import (
        "fmt"
        "sync"
    )

    var wg sync.WaitGroup
    func main() {
        wg.Add(10);
        for i:=0; i<10; i++ {
            // 以匿名函數的形式開啓goroutine
            go func() {
                fmt.Println(i)
                wg.Done()
            }()
        }
        wg.Wait()
    }
  • 在main函數中因爲在匿名函數引用了外部變量i,所以匿名函數以閉包的形式訪問i, 但同時for循環中使用了goroutine,因爲goroutine之間是併發執行的,再參考圖2閉包調用的流程圖,就會出現多個goroutine訪問同一個內存中的變量,會出現「髒讀」現象,代碼編譯執行以下:
2
        7
        7
        3
        7
        10
        10
        10
        10
        7

        Process finished with exit code 0
  • 爲了解決這個問題,也很簡單,咱們只需讓for循環時每一個匿名函數綁定的不是外部變量i,而是i的副本,如何解決呢?之間用匿名函數傳參的形式,因爲go語言的函數傳參都是值傳遞,這樣就能夠經過值傳遞來爲每一個goroutine的匿名函數複製出一個當前i的副本,全部的goroutine在同時執行時互不影響。代碼以下:
package main

        import (
            "fmt"
            "sync"
        )

        var wg sync.WaitGroup
        func main() {
            wg.Add(10);
            for i:=0; i<10; i++ {
                // 以匿名函數的形式開啓goroutine
                go func(num int) {
                    fmt.Println(num)
                    wg.Done()
                }(i)
            }
            wg.Wait()
        }
編譯執行結果以下:
5
        6
        2
        0
        7
        9
        3
        8
        1
        4

        Process finished with exit code 0
能夠發現,每一個goroutine打印的結果都不同,打印順序隨機是因爲goroutine之間的併發執行形成的,咱們經過匿名函數傳參的形式就解決了這種因爲不經意間使用了閉包(本身沒有發現)帶來的錯誤,在go語言的併發編程中,這種狀況比較多見,咱們應該格外注意。

閉包問題的討論就到這裏了,各位看官下期再見~~~

我是lioney,年輕的後端攻城獅一枚,愛鑽研,愛技術,愛分享。
我的筆記,整理不易,感謝閱讀、點贊和收藏。
文章有任何問題歡迎你們指出,也歡迎你們一塊兒交流後端各類問題!
相關文章
相關標籤/搜索