Golang原理分析:闭包及for range延迟绑定问题原理及解决

在这里插入图片描述

1.Golang中的闭包

1.1.什么是闭包

当一个函数引用了环境的变量,被引用的变量与该函数同时存在组成的系统,被称为闭包。
闭包 = 环境的变量 + 函数。
在这里插入图片描述

以下JavaScript代码展示了一个基础的闭包:

  • name是init函数中的内部变量
  • displayName()是init函数中定义的函数
  • displayName()函数引用了name变量
  • displayName() = name + func() = 环境的变量 + 函数,所以displayName是一个引用了name变量的闭包
function init() {
    
    
    var name = "something"; 
    function displayName() {
    
     
        alert(name); 
    }
    displayName();
}
init();

1.2.Golang中使用闭包

Golang中天然支持闭包(函数是一等公民),下面看一个简单的case:

func someMethod() {
    
    
    // 准备一个字符串
    str := "hello world"
    // 创建一个匿名函数
    foo := func() {
    
    
        // 匿名函数中访问str
        str = "hello dude"
    }
    // 调用匿名函数
    foo()
}
  • 第3行,准备一个字符串用于修改
  • 第5行,创建一个匿名函数
  • 第8行,在匿名函数中并没有定义 str,str 的定义在匿名函数之前,此时,str 就被引用到了匿名函数中形成了闭包
  • 第10行,执行闭包,此时 str 发生修改,变为 hello dude。

1.3.闭包的特性

1.3.1.记忆特性(环境绑定)

闭包可以记忆创建时的环境,并且一直保持对该环境的绑定,直到闭包被销毁。

一个累加器的例子:

// 提供一个值, 每次调用函数会指定对值进行累加
func Accumulate(value int) func() int {
    
    
   return func() int {
    
    
      value++
      return value
   }
}
func testAccumulator() {
    
    
   // 创建一个累加器, 初始值为1
   accumulator := Accumulate(1)
   fmt.Printf("%p\n", &accumulator) // 0xc00000e028
   fmt.Println(accumulator())    // 2
   fmt.Println(accumulator())    // 3
   
   // 创建一个累加器, 初始值为10
   accumulator2 := Accumulate(10)
   fmt.Printf("%p\n", &accumulator2) // 0xc00000e038
   fmt.Println(accumulator2()) // 11
}
  • Accumulate函数会创建一个绑定value值的闭包
  • 第一次创建该闭包指定value为1,函数地址为0xc00000e028,进行两次闭包调用可以发现值依次增长为2和3
  • 第二次创建该闭包指定value为10,函数地址为0xc00000e038,进行一次闭包调用可以发现值变为11
  • 以上测试可见,两次生成的闭包依次绑定了自身的环境,并且一直保持绑定

生成指定后缀文件名称的例子:

func BuildFileSuffix(suffix string) func(fileName string) string {
    
    
   return func(fileName string) string {
    
    
      // 闭包绑定环境变量suffix
      // 闭包自身输入fileName,组成完成文件名
      return fmt.Sprintf("%v.%v", fileName, suffix)
   }
}

func testBuildFileSuffix() {
    
    
   // 后缀名java
   javaFunc := BuildFileSuffix("java")
   fmt.Println(javaFunc("file1")) // file1.java
   fmt.Println(javaFunc("file2")) // file2.java
   // 后缀名golang
   golangFunc := BuildFileSuffix("golang")
   fmt.Println(golangFunc("file3"))   // file3.golang
   fmt.Println(golangFunc("file4"))   // file4.golang
}

1.3.2.延迟绑定

闭包会在使用时实际读取环境变量值,而不是取生成闭包时的环境变量值。

下面的例子里在闭包f生成时x的值为1,然后将x值设置为2,此时返回闭包。闭包在使用时x值已经变为2,所以打印的值为2。

func DelayBidding() func() {
    
    
   x := 1
   f := func() {
    
    
      fmt.Println(x)
   }
   x = 2
   return f
}

func testDelayBidding() {
    
    
   DelayBidding()() // 2
}

2.for range延迟绑定问题

在Golang中使用for range时,会存在闭包的延迟绑定问题,该问题是Golang经典问题,在开发时也该格外注意。

2.1.for range复用

for range的惯用法是使用短变量声明方式(:=)在for的initStmt中声明迭代变量(iteration variable)。但需要注意的是,这些迭代变量在for range的每次循环中都会被重用,而不是重新声明:

func forRange() {
    
    
   list := []int{
    
    1, 2, 3, 4, 5}
   for i, v := range list {
    
    
      fmt.Printf("%v-%v\n", &i, &v)
   }
}

输出结果为:

0xc000094008-0xc000094010
0xc000094008-0xc000094010
0xc000094008-0xc000094010
0xc000094008-0xc000094010
0xc000094008-0xc000094010

可见,在使用for range时,生成的i和v都复用的是同一变量,所以for range表达式又可等价为:

func forRange() {
    
    
   list := []int{
    
    1, 2, 3, 4, 5}
   var i, v int
   for i, v = range list {
    
    
      fmt.Printf("%v-%v\n", &i, &v)
   }
}

2.2.for range延迟绑定

所以在for range中使用闭包时,会存在延迟绑定问题,因为i和v本质上是在被复用的,所以闭包被调用时会获取到i和v的最新值:

func TestForRangeDelayBidding() {
    
    
   list := []int{
    
    1, 2, 3, 4, 5}
   var funcList []func()
   for _, v := range list {
    
    
      f := func() {
    
    
         fmt.Println(v)
      }
      funcList = append(funcList, f)
   }
   for _, f := range funcList {
    
    
      f()
   }
}

以上结果预期为1,2,3,4,5,实际输出值为5,5,5,5,5。
再看一个使用go runtine的闭包的例子:

func TestForRangeGoRuntineDelayBidding() {
    
    
   list := []int{
    
    1, 2, 3, 4, 5}
   var wg sync.WaitGroup
   for _, v := range list {
    
    
      wg.Add(1)
      go func() {
    
    
         defer wg.Done()
         fmt.Println(v)
      }()
   }
   wg.Wait()
}

以上结果预期为1,2,3,4,5,实际输出值为5,5,5,5,5。

2.3.如何避免延迟绑定

for range本质上是复用了i和v,所以避免for range闭包延迟绑定的方式就是不让其复用i和v:

  • 第一种方式,在闭包声明前,再创建一个新值给闭包引用,不复用原有值
func TestForRange1() {
    
    
   list := []int{
    
    1, 2, 3, 4, 5}
   var wg sync.WaitGroup
   for _, v := range list {
    
    
      wg.Add(1)
      // 创建一个新值v2,v2不会被复用
      v2 := v
      go func() {
    
    
         defer wg.Done()
         // 闭包引用v2
         fmt.Println(v2)
      }()
   }
   wg.Wait()
}
  • 第二种方式,闭包声明时在指定传入参数,此时发生值拷贝,不复用原有值
func TestForRange2() {
    
    
   list := []int{
    
    1, 2, 3, 4, 5}
   var wg sync.WaitGroup
   for _, v := range list {
    
    
      wg.Add(1)
      // 将值拷贝传入闭包
      go func(v2 int) {
    
    
         defer wg.Done()
         fmt.Println(v2)
      }(v)
   }
   wg.Wait()
}

猜你喜欢

转载自blog.csdn.net/pbrlovejava/article/details/128355818