Golang那些坑-for循环goroutine执行闭包函数

1,264 阅读2分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第1天,点击查看活动详情

PS:已经更文多少天,N就写几。一定要写对文案,否则文章不计入在内

for循环并发执行闭包函数会发生怎样的线上惨剧?资源泄漏,计费亏损?本文从匿名函数、闭包函数区分说起,解析了for-range中并发执行闭包的坑和解决办法。
先来看下以下代码输出是什么:

//程序一
values := []string{"a", "b", "c"}
for _, val := range values {
	go func() {
		fmt.Println(val)
	}()
}
//程序二
values := []string{"a", "b", "c"}
for _, val := range values {
   func() {
      fmt.Println(val)
   }()
}

如果很清楚知道输出并知道为什么这样输出,那可以直接跳过本文。

匿名函数和闭包函数

// 具名函数
func Add(a, b int) int {
    return a+b
}

// 匿名函数
var Add = func(a, b int) int {
    return a+b
}

//闭包函数
func main() {
    for i := 0; i < 3; i++ {
        defer func(){ println(i) } ()
    }
}
// Output:
// 3
// 3
// 3

闭包函数:匿名函数捕获了外部函数的局部变量i,这种函数我们一般叫闭包。闭包对捕获的外部变量并不是传值方式访问,而是以引用的方式访问。在for迭代语句中,每个defer语句延迟执行的函数引用的都是同一个i迭代变量,在循环结束后这个变量的值为3,因此最终输出的都是3。

values := []string{"a", "b", "c"}
for _, val := range values {
   func() {
      fmt.Println(val)
   }()  
}
// Output:
//a
//b
//c

因为是闭包函数,且立即执行了闭包函数,所以输出正常。

values := []string{"a", "b", "c"}
for _, val := range values {
	go func() {
		fmt.Println(val)
	}()
}
// Output:
//c
//c
//c

因为执行闭包函数使用了goroutine执行,无顺序保证; 各goroutine创建后,执行到fmt.Println语句时,val值已是最后一个val=c。如果goroutine里执行的是资源关闭操作,那会造成资源泄漏;如果执行的是收费结算逻辑,那会造成收费重复去重后亏损。

解决方法

values := []string{"a", "b", "c"}
for _, val := range values {
        v := val
	go func() {
		fmt.Println(v)
	}()
}

values := []string{"a", "b", "c"}
for _, val := range values {
	go func(v string) {
		fmt.Println(v)
	}(val)
}

第一种方法是在循环体内部再定义一个局部变量,这样每次迭代语句的闭包函数捕获的都是不同的变量,这些变量的值对应迭代时的值。第二种方式是将迭代变量通过闭包函数的参数传入,语句会马上对调用参数求值。两种方式都是可以工作的。