关键词:
函数式编程 闭包 匿名函数 匿名函数特别适合作为函数或方法的回调
在Go中函数是一等公民,和string,int等一样。 而在C、C++ 等不支持匿名函数的语言中,函数不能在运行期创建
go 学习笔记之仅仅需要一个示例就能讲清楚什么闭包
闭包 与 普通函数的区别
在(普通)函数里面定义一个内部函数(匿名函数),并且这个内部函数(匿名函数)用到了外面(普通)函数的变量 ,那么将这个内部函数和用到的一些变量统称为闭包
匿名函数和闭包的关系
简单来说匿名函数 是指不需要定义函数名的一种函数实现方式。匿名函数是由一个不带函数名的函数声明和函数体组成。匿名函数的优越性在于可以直接使用函数内的变量,不必声明(一个子方法)所以(在某些场景下)被广泛使用
关于闭包的定义存在以下广泛流传的公式:闭包=函数+引用环境。函数指的是匿名函数,引用环境指的是编译器发现闭包,直接将闭包引用的外部变量在堆上分配空间;当闭包引用了函数的内部变量(即局部变量)时,每次调用的外部变量数据都会跟随闭包的变化而变化,闭包函数和外部变量是共享的。 显然,闭包只能通过匿名函数实现,可以把闭包看作是有状态的匿名函数,反过来,如果匿名函数引用了外部变量,就形成了一个闭包
Go 函数式编程篇(三):匿名函数和闭包
一般来说,一个函数返回另外一个函数,这个被返回的函数可以引用外层函数的局部变量,这形成了一个闭包。在Go中,「闭包在实现上是一个结构体,它存储了一个函数(通常是其入口地址)和一个关联的上下文环境(相当于一个符号查找表)」
1 2 3 4 type closure struct { F uintptr x *int }
Go函数闭包底层实现
在Go,PHP中,匿名函数可以认为就是闭包(Go 规范和 FAQ 都这么说了 ),哪怕这个匿名函数没有入参,没有引用外部的变量,也没有任何返回值,如
1 2 3 func () { print (123 ) }()
严格来说,这其实只是个匿名函数, 不算闭包。
但Go里称其为闭包也ok,即模糊了匿名函数和闭包的界限(有引用外部变量的匿名函数为闭包)
一道 Go 闭包题,面试官说原来自己答错了:面别人也涨知识
一些例子
无参数也无返回值的匿名函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package mainimport ( "fmt" ) func main () { f := func () { fmt.Println("不加括号就只是定义,赋值给f,可通过f()来调用" ) } f() fmt.Printf("变量f的类型为: %T\n" , f) fmt.Println("--------------" ) func () { fmt.Println("而加上最后加上()就是直接调用(这种方式只能在此调用一次,没法复用了)" ) }() }
输出:
1 2 3 4 不加括号就只是定义,赋值给f,可通过f()来调用 变量f的类型为: func () -------------- 而加上最后加上()就是直接调用(这种方式只能在此调用一次,没法复用了)
带参数的匿名函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package mainimport ( "fmt" ) func main () { i := 0 func (i int ) { fmt.Println(i + 1 ) }(i) i = -100000 add := func (k int ) { fmt.Println(k + 6 ) } add(200 ) }
输出:
配合defer,可以使问题非常复杂。也是高阶面试常问的~
变形1:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package mainimport ( "fmt" ) func main () { i := 0 defer func (i int ) { fmt.Println(i + 1 ) }(i) i = -100000 add := func (k int ) { fmt.Println(k + 6 ) } add(200 ) }
输出:
目前还好理解,defer在return时执行(确切地说,是在return和计算return值的中间执行)
变形2:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package mainimport ( "fmt" ) func main () { i := 0 defer func (k int ) { fmt.Println(i + 1 ) }(i) i = -100000 add := func (k int ) { fmt.Println(k + 6 ) } add(200 ) }
输出:
如果有人说Go简单,可以请其解释一下这个输出..
有返回值的匿名函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package mainimport "fmt" func main () { name := "张三" say := func (name string ) string { return "hello " + name } res := say(name) fmt.Println(res) }
当返回值是匿名函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package mainimport "fmt" func main () { a := Fun() b := a("hello " ) c := a("hello " ) d := Fun() e := d("hello " ) f := d("hello " ) fmt.Println(b) fmt.Println(c) fmt.Println(e) fmt.Println(f) } func Fun () func (string ) string { rs := "world+" return func (args string ) string { rs += args return rs } }
等同于
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package mainimport "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) fmt.Println(c) fmt.Println(e) fmt.Println(f) }
参考自 GO 匿名函数和闭包
当参数是匿名函数
参考下方回调函数:闭包可以用作回调函数(例如在异步编程中,可以捕获外部函数的上下文) && 高阶函数:闭包可以用作高阶函数的参数,并在调用时返回新的函数?(将匿名函数作为函数参数;可以让该函数执行多种不同逻辑)
多个匿名函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package mainimport "fmt" func main () { f1, f2 := F(1 , 2 ) fmt.Println(f1(4 )) fmt.Println(f2()) } 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 }
常见使用场景
私有数据:闭包可以捕获函数内部的数据,并且对外部不可见。这是一种创建私有数据的方法(保证局部变量的安全性)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package mainimport "fmt" func main () { var j int = 1 f := func () { var i int = 1 fmt.Printf("i, j: %d, %d\n" , i, j) } f() j += 2 f() defer f() j += 10000 }
输出:
1 2 3 i, j: 1 , 1 i, j: 1 , 3 i, j: 1 , 10003
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package mainimport ( "fmt" ) func main () { accumulator := SomeFunc() fmt.Println("The first call CallNum is " , accumulator()) fmt.Println("The second call CallNum is " , accumulator()) } func SomeFunc () func () int { var CallNum = 0 return func () int { CallNum++ return CallNum } }
输出:
1 2 The first call CallNum is 1 The second call CallNum is 2
通过闭包既没有暴露CallNum这个变量,又实现了为函数计数的目的
回调函数:闭包可以用作回调函数(例如在异步编程中,可以捕获外部函数的上下文) && 高阶函数:闭包可以用作高阶函数的参数,并在调用时返回新的函数? (将匿名函数作为函数参数;可以让该函数执行多种不同逻辑)
Go基础系列:函数(2)——回调函数和闭包
参考自 【Go基础】搞懂函数回调和闭包
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外一方调用的,用于对该事件或条件进行响应。 日常开发中,可以将函数B作为另一个函数A的参数,可以使得函数A的通用性更强(可随意定义函数B,只要满足规则,函数A都可以去处理),这比较适合于回调函数。
下面看几个简单的例子来理解回调:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package mainimport "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)) fmt.Println(test1(2 , 3 , calculationAND)) }
再看个简单例子:将字符串转为Int,转换失败时执行回调函数,输出错误信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 package mainimport ( "fmt" "strconv" ) type Callback func (msg string ) 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)) }
输出:
1 2 18 Convert error (转换发生了错误!): strconv.ParseInt: parsing "hh" : invalid syntax
下面这个例子和第一个类似:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package mainimport "fmt" func main () { add1 := func (a, b int ) int { return a + b } 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)) }
输出:
这样就可以通过一个函数执行多种不同加法实现算法,提升代码的复用性
可以基于这个功能特性实现一些更复杂的业务逻辑,如 Go 官方 net/http 包底层的路由处理器 也是这么实现的:
1 2 3 4 5 6 func HandleFunc (pattern string , handler func (ResponseWriter, *Request) ) { DefaultServeMux.HandleFunc(pattern, handler) }
Go源码中还有非常多的将func作为参数的高阶函数,参数的func即回调函数,更多可参考
可通过关键字func(检索
延迟计算:闭包可以延迟计算,直到闭包被调用时才执行计算(将匿名函数作为函数返回值)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package mainimport "fmt" func deferAdd (a, b int ) func () int { return func () int { return a + b } } func main () { addFunc := deferAdd(1 , 2 ) fmt.Println(addFunc()) }
Go函数闭包底层实现
易错问题
循环里打印出的都是最后一个值
case1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 package mainimport "fmt" func main () { 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 ]() a[1 ]() a[2 ]() }
解决办法:
每次复制变量 i 然后传到匿名函数中,让闭包的环境变量不相同。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 package mainimport "fmt" func main () { a := F() a[0 ]() a[1 ]() a[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 }
参考自 GO 匿名函数和闭包
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package mainimport "fmt" func main () { var s []func () for _, v := range []string {"a" , "b" , "c" , "d" , "e" } { s = append (s, func () { fmt.Printf("value: %v\n" , v) }) } for _, f := range s { f() } }
输出:
1 2 3 4 5 value: e value: e value: e value: e value: e
闭包中捕获的v不是”值”, 而是”有地址的变量”(如GoLang闭包,注意!这里有蹊跷 中图1所示),且创建闭包时,循环变量的值已经被确定,并与闭包关联。当闭包被调用时,它使用捕获的值,而不是当前值,解决的关键就在于重新声明变量,这样每个闭包都有自己的变量,能够正确地访问其所需的值
case2(for range+Goroutine 使用闭包不当)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package mainimport ( "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) }
由于没有在Goroutine中对切片执行写操作,所以首先排除了内存屏障的问题,最终还是通过反编译查看汇编代码,发现Goroutine打印的变量v,其实是地址引用,Goroutine执行的时候变量v所在地址所对应的值已经发生了变化,汇编代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 for _, v := range tests1ice { 499224 : 48 8 d 05 f5 af 00 00 lea 0xaff5 (%rip),%rax # 4 a4220 <type .*+0xa220 > 49922 b: 48 89 04 24 mov %rax,(%rsp) 49922 f: e8 8 c 3 a f7 ff callq 40 ccc0 <runtime.newobject> 499234 : 48 8 b 44 24 08 mov 0x8 (%rsp),%rax 499239 : 48 89 44 24 48 mov %rax,0x48 (%rsp) 49923 e: 31 c9 xor %ecx,%ecx 499240 : eb 3 e jmp 499280 <main.main+0xc0 > 499242 : 48 89 4 c 24 18 mov %rcx,0x18 (%rsp) 499247 : 48 8 b 54 cc 20 mov 0x20 (%rsp,%rcx,8 ),%rdx 49924 c: 48 89 10 mov %rdx,(%rax) go func () { 49924 f: c7 04 24 08 00 00 00 movl $0x8 ,(%rsp) 499256 : 48 8 d 15 f3 b7 02 00 lea 0x2b7f3 (%rip),%rdx # 4 c4a50 <go .func .*+0x6c > 49925 d: 48 89 54 24 08 mov %rdx,0x8 (%rsp) 499262 : 48 89 44 24 10 mov %rax,0x10 (%rsp) 499267 : e8 54 3 a fa ff callq 43 ccc0 <runtime.newproc>
解决方案一:在参数方式向匿名函数传递值引用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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) }
解决方案二:在调用gorouinte前将变量进行值拷贝
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package mainimport ( "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) }
Go的闭包看你犯错,Rust却默默帮你排坑
另外的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package mainimport ( "fmt" "time" ) func main () { s := []int {1 , 2 , 3 } for _, v := range s { go func () { fmt.Println(v) }() } time.Sleep(1e9 ) }
无法得到预期结果1,2,3的原因是在没有将变量 v 的拷贝值传进匿名函数之前,只能获取最后一次循环的值,是新手最容易遇到的坑之一。有效规避方式为每次将变量v的拷贝传进函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package mainimport ( "fmt" "time" ) func main () { s := []int {1 , 2 , 3 } for _, v := range s { go func (v int ) { fmt.Println(v) }(v) } time.Sleep(1e9 ) }
搭配defer使用:往defer里传入一个闭包,虽然是值传递,但是拷贝的是函数指针,可以解决一些使用defer会立刻拷贝函数中引用的外部参数引起的时机问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package mainimport "fmt" func main () { x, y := 1 , 2 defer func (a int ) { fmt.Printf("x:%d,y:%d\n" , a, y) }(x) x += 100 y += 100 }
无法得到期待的结果x:1,y:2的原因是:defer 调用会在当前函数执行结束前才被执行,这些调用被称为延迟调用,而defer 中使用匿名函数是一个闭包,y为闭包引用的外部变量会跟着闭包环境变化,当延迟调用时y已经变成102,所以最终输出的y也不再是2了。
有效规避方式只需要去掉defer即可
原文链接: https://dashen.tech/2022/04/11/Go中的匿名函数与闭包/
版权声明: 转载请注明出处.