go 學習筆記之僅僅須要一個示例就能講清楚什麼閉包

本篇文章是 Go 語言學習筆記之函數式編程系列文章的第二篇,上一篇介紹了函數基礎,這一篇文章重點介紹函數的重要應用之一: 閉包前端

空談誤國,實幹興邦,以具體代碼示例爲基礎講解什麼是閉包以及爲何須要閉包等問題,下面咱們沿用上篇文章的示例代碼開始本文的學習吧!編程

斐波那契數列是形如 1 1 2 3 5 8 13 21 34 55遞增數列,即從第三個數開始,後一個數字是前兩個數字之和,保持此規律無限遞增...後端

go-functional-programming-about-fib.png

開門見山,直接給出斐波那契數列生成器,接下來的文章慢慢深挖背後隱藏的奧祕,一個例子講清楚什麼是閉包.數組

「雪之夢技術驛站」: 若是還不瞭解 Go 語言的函數用法,能夠參考上一篇文章: go 學習筆記之學習函數式編程前不要忘了函數基礎閉包

  • Go 版本的斐波那契數列生成器
// 1 1 2 3 5 8 13 21 34 55
// a b
// a b
func fibonacci() func() int {
  a, b := 0, 1
  return func() int {
    a, b = b, a+b
    return a
  }
}
複製代碼

「雪之夢技術驛站」: Go 語言支持連續賦值,更加貼合思考方式,而其他主流的編程語言可能不支持這種方式,大多采用臨時變量暫存的方式.app

  • Go 版本的單元測試用例
// 1 1 2 3 5 8 13 21 34 55
func TestFibonacci(t *testing.T) {
  f := fibonacci()
  for i := 0; i < 10; i++ {
    fmt.Print(f(), " ")
  }
  fmt.Println()
}
複製代碼

「雪之夢技術驛站」: 循環調用 10斐波那契數列生成器,所以生成前十位數列: 1 1 2 3 5 8 13 21 34 55編程語言

背後有故事

小小的斐波那契數列生成器背後蘊藏着豐富的 Go 語言特性,該示例也是官方示例之一.ide

go-functional-programming-fib-try-go.png

  • 支持連續賦值,無需中間變量

「雪之夢技術驛站」: Go 語言和其餘主流的編程語言不一樣,它們大多數最多支持多變量的連續聲明而不支持連續賦值.函數式編程

這也是 Go 語言特有的交換變量方式,a, b = b, a 語義簡單明確並不用引入額外的臨時變量.函數

func TestExchange(t *testing.T) {
  a, b := 1, 2
  t.Log(a,b)

  // 2,1
  a, b = b, a
  t.Log(a,b)
}
複製代碼

「雪之夢技術驛站」: Go 語言實現變量交互的示例,a, b = b, a 表示變量直接交換.

而其餘主流的編程語言的慣用作法是須要引入臨時變量,大多數代碼相似以下方式:

func TestExchange(t *testing.T) {
  a, b := 1, 2
  t.Log(a,b)

  // 2,1
  temp := a
  a = b
  b = temp
  t.Log(a,b)
}
複製代碼

「雪之夢技術驛站」: Go 語言的多變量同時賦值特性體現的更可能是一種聲明式編程思想,不關注具體實現,而引入臨時變量這種體現的則是命令式編程思惟.

  • 函數的返回值也能夠是函數

「雪之夢技術驛站」: Go 語言中的函數是一等公民,不只函數的返回值能夠是函數,參數,變量等等均可以是函數.

函數的返回值能夠是函數,這樣的實際意義在於使用者能夠擁有更大的靈活性,有時能夠用做延遲計算,有時也能夠用做函數加強.

先來演示一下延遲計算的示例:

函數的返回值能夠是函數,由此實現相似於 i++ 效果的自增函數.由於 i 的初值是 0,因此每調用一次該函數, i 的值就會自增,從而實現 i++ 的效果.

func autoIncrease() func() int {
  i := 0
  return func() int {
    i = i + 1
    return i
  }
}
複製代碼

再小的代碼片斷也不該該忘記測試,單元測試繼續走起,順便看一下使用方法.

func TestAutoIncrease(t *testing.T) {
  a := autoIncrease()

  // 1 2 3
  t.Log(a(),a(),a())
}
複製代碼

初始調用 autoIncrease 函數並無直接獲得結果而是返回了函數引用,等到使用者以爲時機成熟後再次調用返回的函數引用即變量a ,這時候纔會真正計算結果,這種方式被稱爲延遲計算也叫作惰性求值.

繼續演示一下功能加強的示例:

由於要演示函數加強功能,沒有輸入哪來的輸出?

因此函數的入參應該也是函數,返回值就是加強後的函數.

這樣的話接下來要作的函數就比較清晰了,這裏咱們定義 timeSpend 函數: 實現的功能是包裝特定類型的函數,增長計算函數運行時間的新功能幷包裝成函數,最後返回出去給使用者.

func timeSpend(fn func()) func() {
  return func()  {
    start := time.Now()

    fn()

    fmt.Println("time spend : ", time.Since(start).Seconds())
  }
}
複製代碼

爲了演示包裝函數 timeSpend,須要定義一個比較耗時函數當作入參,函數名稱姑且稱之爲爲 slowFunc ,睡眠 1s模擬耗時操做.

func slowFunc() {
  time.Sleep(time.Second * 1)

  fmt.Println("I am slowFunc")
}
複製代碼

無測試不編碼,繼續運行單元測試用例,演示包裝函數 timeSpend 是如何加強原函數 slowFunc 以實現功能加強?

func TestSlowFuncTimeSpend(t *testing.T) {
  slowFuncTimeSpend := timeSpend(slowFunc)

  // I am slowFunc
  // time spend : 1.002530902
  slowFuncTimeSpend()
}
複製代碼

「雪之夢技術驛站」: 測試結果顯示原函數 slowFunc 被當作入參傳遞給包裝函數 timeSpend 後實現了功能加強,不只保留了原函數功能還增長了計時功能.

  • 函數嵌套多是閉包函數

不管是引言部分的斐波那契數列生成器函數仍是演示函數返回值的自增函數示例,其實這種形式的函數有一種專業術語稱爲"閉包".

通常而言,函數內部不只存在變量還有嵌套函數,而嵌套函數又引用了這些外部變量的話,那麼這種形式頗有可能就是閉包函數.

什麼是閉包

若是有一句話介紹什麼是閉包,那麼我更願意稱其爲流浪在外的人想念爸爸媽媽!

go-functional-programming-fib-go-home.jpg

若是非要用比較官方的定義去解釋什麼是閉包,那隻好翻開維基百科 看下有關閉包的定義:

In programming languages, a closure, also lexical closure or function closure, is a technique for implementing lexically scoped name binding in a language with first-class functions. Operationally, a closure is a record storing a function[a] together with an environment.[1] The environment is a mapping associating each free variable of the function (variables that are used locally, but defined in an enclosing scope) with the value or reference to which the name was bound when the closure was created.[b] Unlike a plain function, a closure allows the function to access those captured variables through the closure's copies of their values or references, even when the function is invoked outside their scope.

若是可以直接理解英文的同窗能夠略過這部分的中文翻譯,要是不肯意費腦理解英文的小夥伴跟我一塊兒解讀中文吧!

閉包是一種技術

第一句話英文以下:

In programming languages, a closure, also lexical closure or function closure, is a technique for implementing lexically scoped name binding in a language with first-class functions.

相應的中文翻譯:

閉包也叫作詞法閉包或者函數閉包,是函數優先編程語言中用於實現詞法範圍的名稱綁定技術.

概念性定義解釋後可能仍是不太清楚,那麼就用代碼解釋一下什麼是閉包?

「雪之夢技術驛站」: 編程語言千萬種,前端後端和中臺;愛莫能助,大衆化 Js 帶上 Go .

  • Go 實現斐波那契數列生成器

這是開篇引言的示例,直接照搬過來,這裏主要說明 Go 支持閉包這種技術而已,因此並不關心具體實現細節.

func fibonacci() func() int {
  a, b := 0, 1
  return func() int {
    a, b = b, a+b
    return a
  }
}
複製代碼

單元測試用例函數,連續 10 次調用斐波那契數列生成器,輸出斐波那契數列中的前十位數字.

// 1 1 2 3 5 8 13 21 34 55
func TestFibonacci(t *testing.T) {
  f := fibonacci()
  for i := 0; i < 10; i++ {
    fmt.Print(f(), " ")
  }
  fmt.Println()
}
複製代碼
  • Js 實現斐波那契數列生成器

仿照 Go 語言的實現方式寫一個 Js 版本的斐波那契數列生成器,相關代碼以下:

function fibonacci() {
  var a, b;
  a = 0;
  b = 1;
  return function() {
    var temp = a;
    a = b;
    b = temp + b;
    return a;
  }
}
複製代碼

一樣的,仿造測試代碼寫出 Js 版本的測試用例:

// 1 1 2 3 5 8 13 21 34 55
function TestFibonacci() {
  var f = fibonacci();
  for(var i = 0; i < 10; i++ ){
    console.log(f() +" ");
  }
  console.log();
}
複製代碼

不只僅是 JsGo 這兩種編程語言可以實現閉包,實際上不少編程語言都能實現閉包,就像是面向對象編程同樣,也不是某種語言專有的技術,惟一的區別可能就是語法細節上略有不一樣吧,因此記住了: 閉包是一種技術!

閉包存儲了環境

第二句英文以下:

Operationally, a closure is a record storing a function[a] together with an environment.

相應的中文翻譯:

在操做上,閉包是將函數[a]與環境一塊兒存儲的記錄

第一句咱們知道了閉包是一種技術,而如今咱們有知道了閉包存儲了閉包函數所須要的環境,而環境分爲函數運行時所處的內部環境和依賴的外部環境,閉包函數被使用者調用時不會像普通函數那樣丟失環境而是存儲了環境.

若是是普通函數方式打開上述示例的斐波那契數列生成器:

func fibonacciWithoutClosure() int {
  a, b := 0, 1
  a, b = b, a+b
  return a
}
複製代碼

可想而知,這樣確定是不行的,由於函數內部環境是沒法維持的,使用者每次調用 fibonacciWithoutClosure 函數都會從新初始化變量 a,b 的值,於是沒法實現累加自增效果.

// 1 1 1 1 1 1 1 1 1 1 
func TestFibonacciWithoutClosure(t *testing.T) {
  for i := 0; i < 10; i++ {
    fmt.Print(fibonacciWithoutClosure(), " ")
  }
  fmt.Println()
}
複製代碼

很顯然,函數內部定義的變量每次運行函數時都會從新初始化,爲了不這種狀況,在不改變總體實現思路的前提下,只須要提高變量的做用範圍就能實現斐波那契數列生成器函數:

var a, b = 0, 1
func fibonacciWithoutClosure() int {
  a, b = b, a+b
  return a
}
複製代碼

此時再次運行 10斐波那契數列生成器函數,如咱們所願生成前 10 位斐波那契數列.

// 1 1 2 3 5 8 13 21 34 55
func TestFibonacciWithoutClosure(t *testing.T) {
  for i := 0; i < 10; i++ {
    fmt.Print(fibonacciWithoutClosure(), " ")
  }
  fmt.Println()
}
複製代碼

因此說普通函數 fibonacciWithoutClosure 的運行環境要麼是僅僅依賴內部變量維持的獨立環境,每次運行都會從新初始化,沒法實現變量的重複利用;要麼是依賴了外部變量維持的具備記憶功能的環境,解決了從新初始化問題的同時引入了新的問題,那就是必須定義做用範圍更大的外部環境,增長了維護成本.

既然函數內的變量沒法維持而函數外的變量又須要管理,若是能二者結合的話,豈不是皆大歡喜,揚長補短?

go-functional-programming-fib-balance.jpg

對的,閉包基本上就是這種實現思路!

func fibonacci() func() int {
  a, b := 0, 1
  return func() int {
    a, b = b, a+b
    return a
  }
}
複製代碼

斐波那契數列生成器函數 fibonacci返回值是匿名函數,而匿名函數的返回值就是斐波那契數字.

若是不考慮函數內部實現細節,整個函數的語義是十分明確的,使用者初始化調用 fibonacci 函數時獲得返回值是真正的斐波那契生成器函數,用變量暫存起來,當須要生成斐波那契數字的時候再調用剛纔暫存的變量就能真正生成斐波那契數列.

// 1 1 2 3 5 8 13 21 34 55
func TestFibonacci(t *testing.T) {
  f := fibonacci()
  for i := 0; i < 10; i++ {
    fmt.Print(f(), " ")
  }
  fmt.Println()
}
複製代碼

如今咱們再好比如較一下這種形式實現的閉包和普通函數的區別?

  • 閉包函數 fibonacci內部定義了變量 a,b,最終返回的匿名函數中使用了變量 a,b,使用時間接生成斐波那契數字.
  • 普通函數 fibonacciWithoutClosure外部定義了變量 a,b,調用該函數直接生成斐波那契數字.
  • 閉包函數是延遲計算也就是惰性求值而普通函數是當即計算,二者的調用方式不同.

可是若是把視角切換到真正有價值部分,你會發現閉包函數只不過是普通函數嵌套而已!

func fibonacciDeduction() func() int {
  a, b := 0, 1

  func fibonacciGenerator() int {
    a, b = b, a+b
    return a
  }

  return fibonacciGenerator
}
複製代碼

只不過 Go不支持函數嵌套,只能使用匿名函數來實現函數嵌套的效果,因此上述示例是會直接報錯的喲!

go-functional-programming-fib-nested-error.png

可是某些語言是支持函數嵌套的,好比最經常使用的 Js 語言就支持函數嵌套,用 Js 重寫上述代碼以下:

function fibonacciDeduction() {
  var a, b;
  a = 0;
  b = 1;

  function fibonacciGenerator() {
    var temp = a;
    a = b;
    b = temp + b;
    return a
  }

  return fibonacciGenerator
}
複製代碼

斐波那契數列生成器函數是 fibonacciDeduction,該函數內部真正實現生成器功能的倒是 fibonacciGenerator 函數,正是這個函數使用了變量 a,b ,至關於把外部變量打包綁定成運行環境的一部分!

// 1 1 2 3 5 8 13 21 34 55
function TestFibonacciDeduction() {
  var f = fibonacciDeduction();
  for(var i = 0; i < 10; i++ ){
    console.log(f() +" ");
  }
  console.log();
}
複製代碼

「雪之夢技術驛站」: 閉包並非某一種語言特有的技術,雖然各個語言的實現細節上有所差別,但並不妨礙總體理解,正如定義的第二句那樣: storing a **function**[a] together with an **environment**.

環境關聯了自由變量

第三句英文以下:

The environment is a mapping associating each free variable of the function (variables that are used locally, but defined in an enclosing scope) with the value or reference to which the name was bound when the closure was created

相應的中文翻譯:

環境是一種映射,它將函數的每一個自由變量(在本地使用但在封閉範圍內定義的變量)與建立閉包時名稱綁定到的值或引用相關聯。

環境是閉包所處的環境,這裏強調的是外部環境,更確切的說是相對於匿名函數而言的外部變量,像這種被閉包函數使用可是定義在閉包函數外部的變量被稱爲自由變量.

「雪之夢技術驛站」: 因爲閉包函數內部使用了自由變量,因此閉包內部的也就關聯了自由變量的值或引用,這種綁定關係是建立閉包時肯定的,運行時環境也會一直存在並不會發生像普通函數那樣沒法維持環境.

  • 自由變量

這裏使用了一個比較陌生的概念: 自由變量(在本地使用但在封閉範圍內定義的變量)

很顯然,根據括號裏面的註釋說明咱們知道: 所謂的自由變量是相對於閉包函數或者說匿名函數來講的外部變量,因爲該變量的定義不受本身控制,因此對閉包函數本身來講就是自由的,並不受閉包函數的約束!

那麼按照這種邏輯繼續延伸猜想的話,匿名函數內部定義的變量豈不是約束變量?對於閉包函數而言的自由變量對於定義函數來講豈不是約束變量?

var a, b = 0, 1
func fibonacciWithoutClosure() int {
  a, b = b, a+b
  return a
}
複製代碼

「雪之夢技術驛站」: 這裏的變量 a,b 相對於函數 fibonacciWithoutClosure 來講,是否是自由變量?或者說全局變量就是自由變量,對嗎?

  • 值或引用
func fibonacci() func() int {
  a, b := 0, 1
  return func() int {
    a, b = b, a+b
    return a
  }
}
複製代碼

變量 a,b 定義在函數 fibonacci 內部,相對於匿名函數 func() int 來講是自由變量,在匿名函數中直接使用了變量 a,b 並無從新複製一份,因此這種形式的環境關聯的自由變量是引用.

再舉個引用關聯的示例,加深一下閉包的環境理解.

func countByClosureButWrong() []func() int {
  var arr []func() int for i := 1; i <= 3; i++ {
    arr = append(arr, func() int {
      return i
    })
  }
  return arr
}
複製代碼

上述示例的 countByClosureButWrong 函數內部定義了變量數組 arr ,存儲的是匿名函數而匿名函數使用的是循環變量 i .

這裏的循環變量的定義部分是在匿名函數的外部就是所謂的自由變量,變量 i 沒有進行拷貝因此也就是引用關聯.

func TestCountByClosure(t *testing.T) {
  // 4 4 4 
  for _, c := range countByClosureButWrong() {
    t.Log(c())
  }
}
複製代碼

運行這種閉包函數,最終的輸出結果都是 4 4 4,這是由於閉包的環境關聯的循環變量 i引用方式而不是值傳遞方式,因此閉包運行結束後的變量 i 已是 4.

除了引用傳遞方式還有值傳遞方式,關聯自由變量時拷貝一份到匿名函數,使用者調用閉包函數時就能如願綁定到循環變量.

func countByClosureWithOk() []func() int {
  var arr []func() int for i := 1; i <= 3; i++ {
    func(n int) {
      arr = append(arr, func() int {
        return n
      })
    }(i)
  }
  return arr
}
複製代碼

「雪之夢技術驛站」: 自由變量 i 做爲參數傳遞給匿名函數,而 Go 中的參數傳遞只有值傳遞,因此匿名函數使用的變量 n 就能夠正確綁定循環變量了,這也就是自由變量的值綁定方式.

func TestCountByClosureWithOk(t *testing.T) {
  // 1 2 3
  for _, c := range countByClosureWithOk() {
    t.Log(c())
  }
}
複製代碼

「雪之夢技術驛站」: 自由變量經過值傳遞的方式傳遞給閉包函數,實現值綁定環境,正確綁定了循環變量 1 2 3 而不是 4 4 4

訪問被捕獲自由變量

第四句英文以下:

Unlike a plain function, a closure allows the function to access those captured variables through the closure's copies of their values or references, even when the function is invoked outside their scope.

相應的中文翻譯:

與普通函數不一樣,閉包容許函數經過閉包的值的副本或引用訪問那些被捕獲的變量,即便函數在其做用域以外被調用

閉包函數和普通函數的不一樣之處在於,閉包提供一種持續訪問被捕獲變量的能力,簡單的理解就是擴大了變量的做用域.

func fibonacci() func() int {
  a, b := 0, 1
  return func() int {
    a, b = b, a+b
    return a
  }
}
複製代碼

自由變量 a,b 的定義發生在函數 fibonacci 體內,通常而言,變量的做用域也僅限於函數內部,外界是沒法訪問該變量的值或引用的.

可是,閉包提供了持續暴露變量的機制,外界忽然可以訪問本來應該私有的變量,實現了全局變量的做用域效果!

var a, b = 0, 1
func fibonacciWithoutClosure() int {
  a, b = b, a+b
  return a
}
複製代碼

「雪之夢技術驛站」: 普通函數想要訪問變量 a,b 的值或引用,定義在函數內部是沒法暴露給調用者訪問的,只能提高成全局變量才能實現做用域範圍的擴大.

因而可知,一旦變量被閉包捕獲後,外界使用者是能夠訪問這些被捕獲的變量的值或引用的,至關於訪問了私有變量!

怎麼理解閉包

閉包是一種函數式編程中實現名稱綁定的技術,直觀表現爲函數嵌套提高變量的做用範圍,使得本來壽命短暫的局部變量得到長生不死的能力,只要被捕獲到的自由變量一直在使用中,系統就不會回收內存空間!

知乎上關於閉包的衆多回答中,其中有一個回答言簡意賅,特地分享以下:

我叫獨孤求敗,我在一個山洞裏,裏面有世界上最好的劍法,還有最好的武器。我學習了裏面的劍法,拿走了最好的劍。離開了這裏。我來到這個江湖,快意恩仇。可是歷來沒有人知道我這把劍的來歷,和我這一身的武功。。。那山洞就是一個閉包,而我,就是那個山洞裏惟一一個能夠與外界交匯的地方。這山洞的一切對外人而言就像不存在同樣,只有我才擁有這裏面的寶藏!

這也是閉包定義中最後一句話表達的意思: 山洞是閉包函數,裏面的劍法和武器就是閉包的內部環境,而獨孤求敗劍客則是被捕獲的自由變量,他出生在山洞以外的世界,學成歸來後獨自闖蕩江湖.今後江湖上有了獨孤求敗的傳說和那把劍以及神祕莫測的劍法.

go-functional-programming-fib-swordsman.jpeg

掌握閉包了麼

  • 問題: 請將下述普通函數改寫成閉包函數?
func count() []int {
  var arr []int
  for i := 1; i <= 3; i++ {
    arr = append(arr, i)
  }
  return arr
}

func TestCount(t *testing.T) {
  // 1 2 3
  for _, c := range count() {
    t.Log(c)
  }
}
複製代碼
  • 回答: 閉包的錯誤示例以及正確示例
func countByClosureButWrong() []func() int {
  var arr []func() int for i := 1; i <= 3; i++ {
    arr = append(arr, func() int {
      return i
    })
  }
  return arr
}

func TestCountByClosure(t *testing.T) {
  // 4 4 4 
  for _, c := range countByClosureButWrong() {
    t.Log(c())
  }
}

func countByClosureWithOk() []func() int {
  var arr []func() int for i := 1; i <= 3; i++ {
    func(n int) {
      arr = append(arr, func() int {
        return n
      })
    }(i)
  }
  return arr
}

func TestCountByClosureWithOk(t *testing.T) {
  // 1 2 3
  for _, c := range countByClosureWithOk() {
    t.Log(c())
  }
}
複製代碼

那麼,問題來了,本來普通函數就能實現的需求更改爲閉包函數實現後,一不當心就弄錯了,爲何還須要閉包?

閉包概括總結

如今再次回顧一下斐波那契數列生成器函數,相信你已經讀懂了吧,有沒有看到閉包的影子呢?

// 1 1 2 3 5 8 13 21 34 55
// a b
// a b
func fibonacci() func() int {
  a, b := 0, 1
  return func() int {
    a, b = b, a+b
    return a
  }
}
複製代碼

可是,有沒有想過這麼一個問題: 爲何須要閉包,閉包解決了什麼問題?

  • 閉包不是某一種語言特有的機制,但常出如今函數式編程中,尤爲是函數佔據重要地位的編程語言.
  • 閉包的直觀表現是函數內部嵌套了函數,而且內部函數訪問了外部變量,從而使得自由變量得到延長壽命的能力.
  • 閉包中使用的自由變量通常有值傳遞和引用傳遞兩種形式,示例中的斐波那契數列生成器利用的是引用而循環變量示例用的是值傳遞.
  • Go 不支持函數嵌套但支持匿名函數,語法層面的差別性掩蓋不了閉包總體的統一性.

「雪之夢技術驛站」: 因爲篇幅所限,爲何須要閉包以及閉包的優缺點等知識的相關分析打算另開一篇單獨討論,敬請期待...

相關資料參考

若是你以爲本文對你有所幫助,歡迎點贊留言告訴我,你的鼓勵是我繼續創做的動力,不妨順便關注下我的公衆號「雪之夢技術驛站」,按期更新優質文章喲!

雪之夢技術驛站.png
相關文章
相關標籤/搜索