前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【Golang】在Go语言的角度重新审视闭包

【Golang】在Go语言的角度重新审视闭包

作者头像
DDGarfield
发布2022-06-23 19:21:26
4410
发布2022-06-23 19:21:26
举报
文章被收录于专栏:加菲的博客

闭包,最早最早接触到这个概念,是在学习JavaScript的回调函数,引出了闭包的概念,博主从Go语言的角度重新审视闭包,还是从JavaScript当初这个源头说起。

1.JavaScript中的闭包

代码语言: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类型

★委托类型是一个重要概念,向下指代的是函数或者说方法,向上延伸至事件。 ”

说白了,委托就是一个函数类型,杨中科老师把其称为函数占位符,博主喜欢这个说法。

利用委托,博主也写一个类似于JavaScript的回调函数。

代码语言: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等一样的类型。

引用环境:着重要理解一下引用环境,这里博主决定不再讲述概念。直接看一段代码(看完先猜一下输出结果):

代码语言:javascript
复制
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函数中遍历这个函数切片,并执行函数
代码语言:javascript
复制
#输出结果
0xc000014018 2
0xc000014018 2

可以看到地址不变,值也不变,而且值都是退出循环的值。

结论一

闭包=函数+引用环境,这里函数的引用环境就是for循环中i变量,但是i变量是在不断变化的,虽然地址没变,但是延迟到真正使用函数时。值已改变(循环完成)。

类似情况在C#中的Lambda表达式捕获了外部变量,然后延迟执行,一样会出现这种情况:

代码语言:javascript
复制
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语言代码的输出结果,肯定不是我们想要的,既然都循环了,肯定是想让其循环输出,办法来了:

代码语言:javascript
复制
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()
 }
}
代码语言:javascript
复制
0xc000014018 0
0xc000014020 1

结论二

闭包=函数+引用环境,这里函数的引用环境就是for循环内部x变量,但是x变量不同于i:每一次循环就是一次全新的分配空间,赋值。虽然循环已经退出,但是**引用环境(每次不同的x变量)**依然存在。

我们再来看一个有趣的代码,看完接着先猜一下输出结果

代码语言:javascript
复制
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()
}
代码语言:javascript
复制
#输出结果
100
110

答案有没有出乎你的意料,如果没有,恭喜您,下面的可以不看了。如果有,那我们将再一次理解一下:

闭包=函数+引用环境

我们从a,b:=test(100)说起:

  • 执行test函数,经过值拷贝,为x变量分配了空间,拷贝了值100
  • 此时第一个函数内部操作打印了x,并做x+=10,x为其引用环境
  • 第二个函数内部打印x,x也为其引用环境
  • a()时,对x进行打印输出:100,并作100+10,110
  • b()时,对x进行打印输出:110

所以综上,x是值拷贝后,开辟出的空间,这时返回的函数,虽然是不同函数,但是却是同一个引用环境

为了更清晰,再把x的地址输出:

代码语言:javascript
复制
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()
}
代码语言:javascript
复制
#输出结果
100
0xc000014018
110
0xc000014018

4.结论

闭包=函数+引用环境

------------------- End -------------------

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-04-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 加菲的博客 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1.JavaScript中的闭包
  • 2.C#中的闭包
  • 3.Go语言中的闭包
    • 结论一
      • 结论二
      • 4.结论
      相关产品与服务
      消息队列 TDMQ
      消息队列 TDMQ (Tencent Distributed Message Queue)是腾讯基于 Apache Pulsar 自研的一个云原生消息中间件系列,其中包含兼容Pulsar、RabbitMQ、RocketMQ 等协议的消息队列子产品,得益于其底层计算与存储分离的架构,TDMQ 具备良好的弹性伸缩以及故障恢复能力。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档