闭包使用不当会导致什么问题,如何才能去避免

2021-04-07 10:43发布

19条回答
kitidog2016
2楼 · 2021-04-08 09:52

 同事在初始化redis配置的时候,给Dial函数赋值时用了闭包,导致程序上线后,数据怎么都加载不到redis中去,排查了半个多小时,总算找到了罪魁祸首。虽然自己之前对闭包也算了解,但是看到他的那段代码的时候,乍一看竟也没发现出问题来,所以决定写篇文章加深印象,避免自己以后也犯类似的问题。


        先上代码:


func InitRedis() error {

    GRedis = make(map[string]*Cache)

    for _, gConfig := range config.GConfigs.RedisList {  //遍历配置文件,redisList存放了各个redis服务器的ip、端口号以及密码等信息。

        c := new(Cache)

        c.pool = &redis.Pool{

            MaxIdle:     gConfig.Conf.MaxConn,

            MaxActive:   gConfig.Conf.MaxConn,

            IdleTimeout: 240 * time.Second,

            Dial: func() (redis.Conn, error) {   //连接实例的Dial函数,这里用了闭包,其中gConfig为自由变量

                c, err := redis.Dial("tcp", gConfig.Conf.IP+":"+gConfig.Conf.Port, redis.DialConnectTimeout(60*time.Second), redis.DialReadTimeout(60*time.Second), redis.DialWriteTimeout(60*time.Second))  

                if err != nil { 

                    return nil, err

                }

                if gConfig.Conf.Password != "" {

                    if _, err := c.Do("AUTH", gConfig.Conf.Password); err != nil {

                        c.Close()

                        return nil, err

                    }

                }

                return c, err

            },

        }


        c.prefix = gConfig.Conf.Prefix

        GRedis[gConfig.Name] = c

    }


    return nil

}

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

        上述代码已对关键部分(涉及公司机密)进行了删除和处理。

        乍一看是不是很难发现问题到底出在哪,我们当时碰到的问题是,数据没有从db加载到redis服务器1中,经过排查,发现db的数据全加载到了redis服务器3中了。恰巧redis服务器3是配置文件中的最后一个,也就是for循环中遍历的最后一个。进而我们发现Dial函数对应的闭包函数里引用了gConfig这个自由变量。

        这会导致什么问题呢?我们先来温习一下闭包的特性:有自由变量的匿名函数是闭包,闭包的特性,就是内层函数引用了外层函数中的变量,注意是引用哦。

        所以,这会导致redis连接池中每个连接去做Dial的时候,拿到的都是for循环中最后一个gConfig,即连接的都是redis服务器3,去redis服务器1里面拿数据当然就什么都拿不到了。


yjh
3楼 · 2021-04-08 11:06

一个是外部可以读取内部函数的变量,二是变量会始终在内存中,不会被回收机制回收,三是避免全局变量被污染,方便调用上下文的局部变量,加强封装性。

大量使用闭包会导致内存泄露,所以不用的变量应该手动设置为null。


嘿呦嘿呦拔萝卜
4楼 · 2021-04-08 11:10

在初始化redis配置的时候,给Dial函数赋值时用了闭包,导致程序上线后,数据怎么都加载不到redis中去,排查了半个多小时,总算找到了罪魁祸首。虽然自己之前对闭包也算了解,但是看到他的那段代码的时候,乍一看竟也没发现出问题来,所以决定写篇文章加深印象,避免自己以后也犯类似的问题。



猿小猿
5楼 · 2021-04-08 11:25

同事在初始化redis配置的时候,给Dial函数赋值时用了闭包,导致程序上线后,数据怎么都加载不到redis中去,排查了半个多小时,总算找到了罪魁祸首。虽然自己之前对闭包也算了解,但是看到他的那段代码的时候,乍一看竟也没发现出问题来,所以决定写篇文章加深印象,避免自己以后也犯类似的问题。


        先上代码:


func InitRedis() error {

    GRedis = make(map[string]*Cache)

    for _, gConfig := range config.GConfigs.RedisList {  //遍历配置文件,redisList存放了各个redis服务器的ip、端口号以及密码等信息。

        c := new(Cache)

        c.pool = &redis.Pool{

            MaxIdle:     gConfig.Conf.MaxConn,

            MaxActive:   gConfig.Conf.MaxConn,

            IdleTimeout: 240 * time.Second,

            Dial: func() (redis.Conn, error) {   //连接实例的Dial函数,这里用了闭包,其中gConfig为自由变量

                c, err := redis.Dial("tcp", gConfig.Conf.IP+":"+gConfig.Conf.Port, redis.DialConnectTimeout(60*time.Second), redis.DialReadTimeout(60*time.Second), redis.DialWriteTimeout(60*time.Second))  

                if err != nil { 

                    return nil, err

                }

                if gConfig.Conf.Password != "" {

                    if _, err := c.Do("AUTH", gConfig.Conf.Password); err != nil {

                        c.Close()

                        return nil, err

                    }

                }

                return c, err

            },

        }


        c.prefix = gConfig.Conf.Prefix

        GRedis[gConfig.Name] = c

    }


    return nil

}

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

        上述代码已对关键部分(涉及公司机密)进行了删除和处理。

        乍一看是不是很难发现问题到底出在哪,我们当时碰到的问题是,数据没有从db加载到redis服务器1中,经过排查,发现db的数据全加载到了redis服务器3中了。恰巧redis服务器3是配置文件中的最后一个,也就是for循环中遍历的最后一个。进而我们发现Dial函数对应的闭包函数里引用了gConfig这个自由变量。

        这会导致什么问题呢?我们先来温习一下闭包的特性:有自由变量的匿名函数是闭包,闭包的特性,就是内层函数引用了外层函数中的变量,注意是引用哦。

        所以,这会导致redis连接池中每个连接去做Dial的时候,拿到的都是for循环中最后一个gConfig,即连接的都是redis服务器3,去redis服务器1里面拿数据当然就什么都拿不到了。


cccc
6楼 · 2021-04-08 11:54

 同事在初始化redis配置的时候,给Dial函数赋值时用了闭包,导致程序上线后,数据怎么都加载不到redis中去,排查了半个多小时,总算找到了罪魁祸首。虽然自己之前对闭包也算了解,但是看到他的那段代码的时候,乍一看竟也没发现出问题来,所以决定写篇文章加深印象,避免自己以后也犯类似的问题。


三岁奶猫
7楼 · 2021-04-08 13:23

闭包(closure)是 JavasSript 的一大难点,也是它的特色。很多高级应用都要依靠闭包来实现。


变量作用域

要理解闭包,首先要理解 JavasSript 的特殊的变量作用域。


变量的作用域无非就两种:全局变量和局部变量。


JavasSript 语言的特别之处就在于:函数内部可以直接读取全局变量,但是在函数外部无法读取函数内部的局部变量。


注意点:在函数内部声明变量的时候,一定要使用 var 命令。如果不用的话,你实际上声明的是一个全局变量!


是开心果呀 - 热爱生活
8楼 · 2021-04-08 14:37

闭包的功能强大,但如果没有正确理解闭包的概念,其结果往往出乎人的意料。例如,下面是一个较常见的问题:


    
第一个

    
第二个

    
第三个

    
第四个


[removed]
    function test()
    {
            var els = document.getElementById("test").getElementsByTagName("div");
            for (var i = 0; i < els>
        {
             var div = els[i];
              div.onclick = function()
          {
              alert(div[removed]);
              return false;
           }
          }
    }
    test();
[removed]
无论我们点击哪个div,反馈的都是第4个div的内容。究其原因,在于每个div的点击事件都与test方法形成了闭包,且每个div的点击事件都共享同一个闭包作用域链。当事件被触发时,变量i所代表的下标已经指向第4个div。可以采用以下几种方式避免由于闭包引起的问题。
(1)使用this转换闭包的作用域链上下文,上例的闭包可以改写为:
for (var i = 0; i < els>
{
        var div = els[i];
        div.onclick = function()
    {
        alert(this[removed]);
        return false;
       }
}
当点击div的事件被触发时,查找的作用域已经是“this”所指定的上下文。尽管该事件仍然处于“test”闭包内,但由于不访问或不使用闭包的上下文环境,也就不存在由于闭包作用域内变量被引用所引发的问题。
(2)使点击div的事件与for循环形成闭包,而使得for循环内的变量div不被回收。如:
//for循环内定义闭包方法
for (var i = 0; i < els>
{
         var div = els[i];
         a(div);
    function a(o)
    {
       o.onclick = function()
       {
          alert(o[removed]);
       }
    }
}
//for循环外定义闭包方法
for (var i = 0; i < els>
{
       var div = els[i];
       a(div);    
}
function a(o)
{
    o.onclick = function()
    {
          alert(o[removed]);
    }
}
//使用匿名方法,其原理与for循环内定义类似
for (var i = 0; i < els>
{
       var div = els[i];
       (function(o)
    {        
          o.onclick = function()
          {
              alert(o[removed]);
          }
    })(div);
}
通过中间方法a或者匿名方法,使for循环体与onclick事情产生闭包。
(3)控制变量的作用域,使点击div的事件所需变量与外层作用域无关。如:
for (var i = 0; i < els>
{      
      (function()
      {    
        var div = els[i];    
        div.onclick = function()
        {
            alert(div[removed]);
        }
      })();
}
内部函数自身也可能有内部函数。每次作用域链嵌套,都会增加由创建内部函数对象的执行环境所引发的新活动对象。ECMA262规范要求作用域链是临时性的,但对作用域链的长度却没有加以限制。闭包的潜规则即Function与内部定义的Function之间的相互作用域链上下文环境的关系。

灰机带翅膀
9楼 · 2021-04-08 16:10

闭包的特性:

有自由变量的匿名函数是闭包,闭包的特性,就是内层函数引用了外层函数中的变量,注意是引用哦。

        

所以,这会导致redis连接池中每个连接去做Dial的时候,拿到的都是for循环中最后一个gConfig,即连接的都是

redis服务器3,去redis服务器1里面拿数据当然就什么都拿不到了。