【Golang】在Go语言的角度重新审视闭包
闭包,最早最早接触到这个概念,是在学习JavaScript
的回调函数,引出了闭包的概念,博主从Go
语言的角度重新审视闭包,还是从JavaScript
当初这个源头说起。
1.JavaScript中的闭包
function cal(a,b,callback){
var res=(a+b)*100;
return callback(res)
}
cal(1,2,function(res){
console.log(res)
})
一个函数和对其周围状态(
lexical environment
,词法环境**)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure
)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在JavaScript
中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。**
这是MDN
上关于闭包的定义,您理解了吗?
2.C#中的闭包
闭包离不开函数,C#
没有返回函数类型的概念,直愣愣的返回函数肯定是不行,但是C#
创造性了引入了委托delegate
类型
委托类型是一个重要概念,向下指代的是函数或者说方法,向上延伸至事件。
说白了,委托就是一个函数类型,老赵(.net老师)把其称为函数占位符,博主喜欢这个说法。
利用委托,博主也写一个类似于JavaScript
的回调函数。
static void Main(string[] args)
{
var Res = Cal(1, 2);
Console.WriteLine(Res());
Cal(1, 2, (int res) =>
{
Console.WriteLine(res);
});
}
static void Cal(int a, int b, Action<int> calAction)
{
int weight = 100;
calAction((a + b) * weight);
}
这种形式的回调函数,虽然C#
中不爱这么称呼,在一些中间件中被大量使用,用于传递一些配置参数。
用到了Cal
函数内部的乘法因子weight
,并向调用者暴露了内部运算结果。
3.Go语言中的闭包
在Go语言中,我们将再次简练定义闭包:
闭包=函数+引用环境
函数:没什么说的,在Go语言中,就是一种类型,开发者可以把其视作int64
string
等一样的类型。
引用环境:着重要理解一下引用环境,这里博主决定不再讲述概念。直接看一段代码(看完先猜一下输出结果):
package main
func test() []func() {
var funs []func()
for i:=0;i<2 ;i++ {
funs = append(funs, func() {
println(&i,i)
})
}
return funs
}
func main(){
funs:=test()
for _,f:=range funs{
f()
}
}
test()
函数返回一个函数类型的切片- 这个函数功能:
- 打印
for
循环中变量i
的地址与i
的值
- 打印
- main函数中遍历这个函数切片,并执行函数
#输出结果
0xc000014018 2
0xc000014018 2
可以看到地址不变,值也不变,而且值都是退出循环的值。
结论一
闭包=函数+引用环境
,这里函数的引用环境就是for
循环中i
变量,但是i
变量是在不断变化的,虽然地址没变,但是延迟到真正使用函数时。值已改变(循环完成)。
类似情况在C#
中的Lambda
表达式捕获了外部变量,然后延迟执行,一样会出现这种情况:
Action[] actions = new Action[3];
for (int i = 0; i < 3; i++)
{
actions[i] = () =>
{
Console.Write(i);
};
}
foreach (Action a in actions)
{
a();
}
在另一篇博文【Node.js】匿名函数-闭包-Promise也有阐述。
上面Go语言代码的输出结果,肯定不是我们想要的,既然都循环了,肯定是想让其循环输出,办法来了:
package main
func test() []func() {
var funs []func()
for i := 0; i < 2; i++ {
x := i
funs = append(funs, func() {
println(&x, x)
})
}
return funs
}
func main() {
funs := test()
for _, f := range funs {
f()
}
}
0xc000014018 0
0xc000014020 1
结论二
闭包=函数+引用环境
,这里函数的引用环境就是for循环内部x
变量,但是x变量不同于i
:每一次循环就是一次全新的分配空间,赋值。虽然循环已经退出,但是**引用环境(每次不同的x变量)**依然存在。
我们再来看一个有趣的代码,看完接着先猜一下输出结果
package main
func test(x int) (func(),func()) {
return func() {
println(x)
x+=10
}, func() {
println(x)
}
}
func main() {
a,b:=test(100)
a()
b()
}
#输出结果
100
110
答案有没有出乎你的意料,如果没有,恭喜您,下面的可以不看了。如果有,那我们将再一次理解一下:
闭包=函数+引用环境
我们从a,b:=test(100)
说起:
- 执行
test
函数,经过值拷贝,为x
变量分配了空间,拷贝了值100
- 此时第一个函数内部操作打印了x,并做
x+=10
,x为其引用环境 - 第二个函数内部打印x,x也为其引用环境
a()
时,对x进行打印输出:100,并作100+10,110b()
时,对x进行打印输出:110
所以综上,x是值拷贝后,开辟出的空间,这时返回的函数,虽然是不同函数,但是却是同一个引用环境。
为了更清晰,再把x的地址输出:
package main
func test(x int) (func(), func()) {
return func() {
println(x)
x += 10
println(&x)
}, func() {
println(x)
println(&x)
}
}
func main() {
a, b := test(100)
a()
b()
}
#输出结果
100
0xc000014018
110
0xc000014018
4.结论
闭包=函数+引用环境
- 原文作者:Garfield
- 原文链接:http://www.randyfield.cn/post/2021-04-26-go-closure/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。