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