Go 中的匿名函數與閉包

閉包 與 普通函數的區別

在 (普通) 函數里面定義一個內部函數(匿名函數),並且這個內部函數 (匿名函數) 用到了外面 (普通) 函數的變量,那麼將這個內部函數和用到的一些變量統稱爲閉包


匿名函數和閉包的關係

簡單來說匿名函數是指不需要定義函數名的一種函數實現方式。匿名函數是由一個不帶函數名的函數聲明和函數體組成。匿名函數的優越性在於可以直接使用函數內的變量,不必聲明(一個子方法)所以(在某些場景下)被廣泛使用

關於閉包的定義存在以下廣泛流傳的公式:閉包 = 函數 + 引用環境。函數指的是匿名函數,引用環境指的是編譯器發現閉包,直接將閉包引用的外部變量在堆上分配空間;當閉包引用了函數的內部變量(即局部變量)時,每次調用的外部變量數據都會跟隨閉包的變化而變化,閉包函數和外部變量是共享的。
顯然,閉包只能通過匿名函數實現,可以把閉包看作是有狀態的匿名函數,反過來,如果匿名函數引用了外部變量,就形成了一個閉包

一般來說,一個函數返回另外一個函數,這個被返回的函數可以引用外層函數的局部變量,這形成了一個閉包。在 Go 中,「閉包在實現上是一個結構體,它存儲了一個函數(通常是其入口地址)和一個關聯的上下文環境(相當於一個符號查找表)」

type closure struct {
   F uintptr   // 函數指針,代表着內部匿名函數
   x *int      // 自由變量x,代表着對外部環境的引用
}

在 Go,PHP 中,匿名函數可以認爲就是閉包 (Go 規範和 FAQ 都這麼說了),哪怕這個匿名函數沒有入參,沒有引用外部的變量,也沒有任何返回值,如

func(){
    print(123)
    }()

嚴格來說,這其實只是個匿名函數, 不算閉包。

但 Go 裏稱其爲閉包也 ok,即模糊了匿名函數和閉包的界限 (有引用外部變量的匿名函數爲閉包)


一些例子

無參數也無返回值的匿名函數

package main

import (
 "fmt"
)

func main() {

 f := func() {
  fmt.Println("不加括號就只是定義,賦值給f,可通過f()來調用")
 }

 f()
 fmt.Printf("變量f的類型爲: %T\n", f) // func()

 
 
 
 // 下面這種方式定義,只在此調用一次,不如上面的方式,可以隨時複用
 fmt.Println("--------------")
 func() {
  fmt.Println("而加上最後加上()就是直接調用(這種方式只能在此調用一次,沒法複用了)")
 }()

}

輸出:

不加括號就只是定義,賦值給f,可通過f()來調用
變量f的類型爲: func()
--------------
而加上最後加上()就是直接調用(這種方式只能在此調用一次,沒法複用了)

帶參數的匿名函數

package main

import (
 "fmt"
)

func main() {

 i := 0
 // 後面有(),一次執行
 func(i int) {
  fmt.Println(i + 1)
 }(i)

 i = -100000

 
 
 // 賦值給add,可通過add()方式多次調用
 add := func(k int) {
  fmt.Println(k + 6)
 }
 add(200)

}

輸出:

1
206

配合 defer,可以使問題非常複雜。也是高階面試常問的~

變形 1:

package main

import (
 "fmt"
)

func main() {

 i := 0
 // 後面有(),一次執行
 defer func(i int) {
  fmt.Println(i + 1)
 }(i)

 i = -100000

 // 賦值給add,可通過add()方式多次調用
 add := func(k int) {
  fmt.Println(k + 6)
 }
 add(200)

}

輸出:

206
1

目前還好理解,defer 在 return 時執行 (確切地說,是在 return 和計算 return 值的中間執行)

變形 2:

package main

import (
 "fmt"
)

func main() {

 i := 0
 // 後面有(),一次執行
 defer func(k int) {
  fmt.Println(i + 1)
 }(i)

 i = -100000

 // 賦值給add,可通過add()方式多次調用
 add := func(k int) {
  fmt.Println(k + 6)
 }
 add(200)

}

輸出:

206
-99999

如果有人說 Go 簡單,可以請其解釋一下這個輸出..

有返回值的匿名函數

package main

import "fmt"

func main() {

 name := "張三"

 say := func(name string) string {
  return "hello " + name
 }

 res := say(name)

 fmt.Println(res) //hello 張三
}

當返回值是匿名函數

package main

import "fmt"

func main() {

 a := Fun()

 b := a("hello ")
 c := a("hello ")

 d := Fun()
 e := d("hello ")
 f := d("hello ")
 fmt.Println(b) //world+hello
 fmt.Println(c) //world+hello hello
 fmt.Println(e) //world+hello
 fmt.Println(f) //world+hello hello
}
func Fun() func(string) string {
 rs := "world+"
 return func(args string) string {
  rs += args
  return rs
 }
}

等同於

package main

import "fmt"

func main() {

 cui := func() func(string) string {
  rs := "world+"
  return func(args string) string {
   rs += args
   return rs
  }
 }

 a := cui()
 b := a("hello ")
 c := a("hello ")

 d := cui()
 e := d("hello ")
 f := d("hello ")
 fmt.Println(b) //world+hello
 fmt.Println(c) //world+hello hello
 fmt.Println(e) //world+hello
 fmt.Println(f) //world+hello hello
}

參考自 GO 匿名函數和閉包 [1]

當參數是匿名函數

參考下方 [回調函數:閉包可以用作回調函數 (例如在異步編程中,可以捕獲外部函數的上下文) && 高階函數:閉包可以用作高階函數的參數,並在調用時返回新的函數?(將匿名函數作爲函數參數;可以讓該函數執行多種不同邏輯)]( "回調函數:閉包可以用作回調函數 (例如在異步編程中,可以捕獲外部函數的上下文) && 高階函數:閉包可以用作高階函數的參數,並在調用時返回新的函數?(將匿名函數作爲函數參數;可以讓該函數執行多種不同邏輯)")

多個匿名函數

package main

import "fmt"

func main() {
 f1, f2 := F(1, 2)
 fmt.Println(f1(4)) //6
 fmt.Println(f2())  //6
}
func F(x, y int) (func(int) int, func() int) {

 f1 := func(z int) int {
  return (x + y) * z / 2
 }

 f2 := func() int {
  return 2 * (x + y)
 }
 return f1, f2
}

常見使用場景

私有數據:閉包可以捕獲函數內部的數據,並且對外部不可見。這是一種創建私有數據的方法(保證局部變量的安全性)

package main

import "fmt"

func main() {

 var j int = 1

 f := func() {
  var i int = 1 // i 在閉包內部定義,其值被隔離,不能從外部修改
  fmt.Printf("i, j: %d, %d\n", i, j)
 }

 f()
 j += 2
 f() // 對比下面的輸出,可見並不是調用時刻的值,而只是記錄變量的引用

 defer f()
 j += 10000
}

輸出:

i, j: 1, 1
i, j: 1, 3
i, j: 1, 10003
package main

import (
 "fmt"
)

func main() {
 accumulator := SomeFunc() //使用accumulator變量接收一個閉包

 // 累加計數並打印
 fmt.Println("The first call CallNum is ", accumulator()) //運行結果爲:The first call CallNum is 1
 // 累加計數並打印
 fmt.Println("The second call CallNum is ", accumulator()) //運行結果爲:The second call CallNum is 2
}

func SomeFunc() func() int { // 創建一個函數,返回一個閉包,閉包每次調用函數會對函數內部變量進行累加
 var CallNum = 0 //函數調用次數,系函數內部變量,外部無法訪問,僅當函數被調用時進行累加

 return func() int { // 返回一個閉包
  CallNum++ //對value進行累加
  //實現函數具體邏輯
  return CallNum // 返回內部變量value的值
 }
}

輸出:

The first call CallNum is  1
The second call CallNum is  2

通過閉包既沒有暴露 CallNum 這個變量,又實現了爲函數計數的目的

回調函數:閉包可以用作回調函數 (例如在異步編程中,可以捕獲外部函數的上下文) && 高階函數:閉包可以用作高階函數的參數,並在調用時返回新的函數?(將匿名函數作爲函數參數;可以讓該函數執行多種不同邏輯)

Go 基礎系列:函數 (2)——回調函數和閉包 [2]

參考自 【Go 基礎】搞懂函數回調和閉包 [3]

回調函數就是一個通過函數指針調用的函數。如果你把函數的指針(地址)作爲參數傳遞給另一個函數,當這個指針被用來調用其所指向的函數時,就說這是回調函數。回調函數不是由該函數的實現方直接調用,而是在特定的事件或條件發生時由另外一方調用的,用於對該事件或條件進行響應。
日常開發中,可以將函數 B 作爲另一個函數 A 的參數,可以使得函數 A 的通用性更強(可隨意定義函數 B,只要滿足規則,函數 A 都可以去處理),這比較適合於回調函數。

下面看幾個簡單的例子來理解回調:

package main

import "fmt"

type Callback func(x, y int) int

// 提供一個接口,讓外部去實現
func test1(x, y int, callback Callback) int {
 return callback(x, y)
}

// 回調函數的具體實現
func calculationXOR(x, y int) int {
 return x ^ y
}
func calculationAND(x, y int) int {
 return x & y
}

// 回調函數的具體實現
func main() {
 fmt.Println(test1(2, 3, calculationXOR)) //這樣調用test1就能實現異或 以及 與的運算
 fmt.Println(test1(2, 3, calculationAND))
}
1
2

再看個簡單例子:將字符串轉爲 Int,轉換失敗時執行回調函數,輸出錯誤信息

package main

import (
 "fmt"
 "strconv"
)

type Callback func(msg string)

// 將字符串轉換爲int64,如果轉換失敗調用Callback
func stringToInt(s string, callback Callback) int64 {
 if value, err := strconv.ParseInt(s, 0, 0); err != nil {
  callback(err.Error())
  return 0
 } else {
  return value
 }
}

// 記錄日誌消息的具體實現
func errLog(msg string) {
 fmt.Println("Convert error(轉換髮生了錯誤!): ", msg)
}

func main() {
 fmt.Println(stringToInt("18", errLog))
 fmt.Println(stringToInt("hh", errLog))
}

輸出:

18
Convert error(轉換髮生了錯誤!):  strconv.ParseInt: parsing "hh": invalid syntax

下面這個例子和第一個類似:

package main

import "fmt"

func main() {

 // 普通的加法操作
 add1 := func(a, b int) int {
  return a + b
 }

 // 定義另一種加法規則(即  加數*10+第二個加數)
 base := 10
 add2 := func(a, b int) int {
  return a*base + b
 }

 handleAdd(1, 2, add1)
 handleAdd(1, 2, add2)
}

// 將匿名函數作爲參數
func handleAdd(a, b int, call func(int, int) int) {
 fmt.Println(call(a, b))
}

輸出:

3
12

這樣就可以通過一個函數執行多種不同加法實現算法,提升代碼的複用性

可以基於這個功能特性實現一些更復雜的業務邏輯,如 Go 官方 net/http 包底層的路由處理器 [4] 也是這麼實現的:

// HandleFunc registers the handler function for the given pattern
// in the DefaultServeMux.
// The documentation for ServeMux explains how patterns are matched.
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
 DefaultServeMux.HandleFunc(pattern, handler)
}

Go 源碼中還有非常多的將 func 作爲參數的高階函數,參數的 func 即回調函數,更多可參考

可通過關鍵字func(檢索

延遲計算:閉包可以延遲計算,直到閉包被調用時才執行計算 (將匿名函數作爲函數返回值)

package main

import "fmt"

// 將函數作爲返回值
func deferAdd(a, b int) func() int {
 return func() int {
  return a + b
 }
}

func main() {

 // 此時返回的是匿名函數
 addFunc := deferAdd(1, 2)

 // 這裏纔會真正執行加法操作
 fmt.Println(addFunc()) // 3

}

易錯問題

循環裏打印出的都是最後一個值

case1

package main

import "fmt"

func main() {

 // 此時a是F()的返回值,即一個[]func()
 //a := F()
 a := func() []func() {
  b := make([]func(), 3, 3)
  for i := 0; i < 3; i++ {
   b[i] = func() {
    fmt.Println(&i, i)
   }
  }
  return b
 }()

 a[0]( "0") //0x140000200c8 3
 a[1]( "1") //0x140000200c8 3
 a[2]( "2") //0x140000200c8 3
}

//func F() []func() {
// b := make([]func(), 3, 3)
// for i := 0; i < 3; i++ {
//  b[i] = func() {
//   fmt.Println(&i, i)
//  }
// }
// return b
//}

解決辦法:

每次複製變量 i 然後傳到匿名函數中,讓閉包的環境變量不相同。

package main

import "fmt"

func main() {
 a := F()
 a[0]( "0") //0x14000128008 0
 a[1]( "1") //0x14000128010 1
 a[2]( "2") //0x14000128018 2
}
func F() []func() {
 b := make([]func(), 3, 3)
 for i := 0; i < 3; i++ {
  b[i] = (func(j int) func() {
   return func() {
    fmt.Println(&j, j)
   }
  })(i)
 }
 return b
}

//或者
//package main
//
//import "fmt"
//
//func main() {
// a := F()
// a[0]( "0") //0xc00004c080 0
// a[1]( "1") //0xc00004c088 1
// a[2]( "2") //0xc00004c090 2
//}
//func F() []func() {
// b := make([]func(), 3, 3)
// for i := 0; i < 3; i++ {
//  j := i
//  b[i] = func() {
//   fmt.Println(&j, j)
//  }
// }
// return b
//}

參考自 GO 匿名函數和閉包 [5]

package main

import "fmt"

func main() {
 // 保存函數閉包
 var s []func()

 for _, v := range []string{"a""b""c""d""e"} {
  s = append(s, func() {
   // 捕獲v, 保存在閉包中
   fmt.Printf("value: %v\n", v)
  })
 }

 for _, f := range s {
  f()
 }
}

輸出:

value: e
value: e
value: e
value: e
value: e

閉包中捕獲的 v 不是 "值", 而是 "有地址的變量"(如 GoLang 閉包,注意!這裏有蹊蹺 中圖 1 所示),且創建閉包時,循環變量的值已經被確定,並與閉包關聯。當閉包被調用時,它使用捕獲的值,而不是當前值,解決的關鍵就在於重新聲明變量,這樣每個閉包都有自己的變量,能夠正確地訪問其所需的值

case2(for range+Goroutine 使用閉包不當)

package main

import (
 "fmt"
 "time"
)

func main() {
 tests1ice := []int{1, 2, 3, 4, 5}
 for _, v := range tests1ice {

  go func() {
   fmt.Println(v)
  }()
 }
 time.Sleep(2 * time.Second)
}
5
5
5
5
5

由於沒有在 Goroutine 中對切片執行寫操作,所以首先排除了內存屏障的問題,最終還是通過反編譯查看彙編代碼,發現 Goroutine 打印的變量 v,其實是地址引用,Goroutine 執行的時候變量 v 所在地址所對應的值已經發生了變化,彙編代碼如下:

for _, v := range tests1ice {
  499224:       48 8d 05 f5 af 00 00    lea    0xaff5(%rip),%rax        # 4a4220 <type.*+0xa220>
  49922b:       48 89 04 24             mov    %rax,(%rsp)
  49922f:       e8 8c 3a f7 ff          callq  40ccc0 <runtime.newobject>
  499234:       48 8b 44 24 08          mov    0x8(%rsp),%rax
  499239:       48 89 44 24 48          mov    %rax,0x48(%rsp)
  49923e:       31 c9                   xor    %ecx,%ecx
  499240:       eb 3e                   jmp    499280 <main.main+0xc0>
  499242:       48 89 4c 24 18          mov    %rcx,0x18(%rsp)
  499247:       48 8b 54 cc 20          mov    0x20(%rsp,%rcx,8),%rdx
  49924c:       48 89 10                mov    %rdx,(%rax)
                go func() {
  49924f:       c7 04 24 08 00 00 00    movl   $0x8,(%rsp)
  499256:       48 8d 15 f3 b7 02 00    lea    0x2b7f3(%rip),%rdx        # 4c4a50 <go.func.*+0x6c>
  49925d:       48 89 54 24 08          mov    %rdx,0x8(%rsp)
  499262:       48 89 44 24 10          mov    %rax,0x10(%rsp)
  499267:       e8 54 3a fa ff          callq  43ccc0 <runtime.newproc>

解決方案一:在參數方式向匿名函數傳遞值引用

package main

import (
  "fmt"
  "time"
)

func main() {
  tests1ice := []int{1, 2, 3, 4, 5}
  for _, v := range tests1ice {
   w := v
   go func(w int) {
    fmt.Println(w)
   }(w)
  }
  time.Sleep(time.Second)
}
2
4
5
1
3

解決方案二:在調用 gorouinte 前將變量進行值拷貝

package main

import (
 "fmt"
 "time"
)

func main() {
 tests1ice := []int{1, 2, 3, 4, 5}
 for _, v := range tests1ice {
  w := v
  go func() {
   fmt.Println(w)
  }()
 }
 time.Sleep(time.Second)
}
1
3
2
5
4

另外的例子:

package main

import (
 "fmt"
 "time"
)

func main() {
 s := []int{1, 2, 3}
 for _, v := range s {
  go func() {
   fmt.Println(v) // 輸出結果3 3 3
  }()
 }

 time.Sleep(1e9)
}

無法得到預期結果 1,2,3 的原因是在沒有將變量 v 的拷貝值傳進匿名函數之前,只能獲取最後一次循環的值, 是新手最容易遇到的坑之一。有效規避方式爲每次將變量 v 的拷貝傳進函數:

package main

import (
 "fmt"
 "time"
)

func main() {

 s := []int{1, 2, 3}
 for _, v := range s {
  go func(v int) {
   fmt.Println(v) // 輸出結果1,2,3或 1,3,2 或其他順序
  }(v)
 }

 time.Sleep(1e9)
}

搭配 defer 使用:往 defer 裏傳入一個閉包,雖然是值傳遞,但是拷貝的是函數指針,可以解決一些使用 defer 會立刻拷貝函數中引用的外部參數引起的時機問題。

package main

import "fmt"

func main() {
 x, y := 1, 2

 defer func(a int) {
  fmt.Printf("x:%d,y:%d\n", a, y) // y 爲閉包引用,最終結果爲x:1,y:102
 }(x) // 複製 x 的值

 x += 100
 y += 100
}

無法得到期待的結果 x:1,y:2 的原因是:defer 調用會在當前函數執行結束前才被執行,這些調用被稱爲延遲調用,而 defer 中使用匿名函數是一個閉包,y 爲閉包引用的外部變量會跟着閉包環境變化,當延遲調用時 y 已經變成 102,所以最終輸出的 y 也不再是 2 了。

有效規避方式只需要去掉 defer 即可

參考資料

[1]

GO 匿名函數和閉包: https://segmentfault.com/a/1190000018689134

[2]

Go 基礎系列:函數 (2)——回調函數和閉包 : https://www.cnblogs.com/f-ck-need-u/p/9878898.html

[3]

【Go 基礎】搞懂函數回調和閉包: https://blog.csdn.net/dl962454/article/details/123460053

[4]

路由處理器: https://github.com/golang/go/blob/master/src/net/http/server.go#L2573

[5]

GO 匿名函數和閉包: https://segmentfault.com/a/1190000018689134

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/iAPLIs0i-9P8KUCUCC3StA