记一次工程项目中goroutine引用闭包的错误操作

在进行敏捷开发项目中,同事写出来一些bug,其中有个goroutine引用闭包的问题,当时由于嵌入到了实际业务中相对环境较为复杂,我还没怎么注意到,后来下班地铁上就想起来了和一道经常考的面试题几乎一致。

来看一道经典的闭关面试题:

关于 for 循坏中 嵌入了 goroutine 的问题:

goroutine 与 for 的打印结果和顺序

func main() {
    runtime.GOMAXPROCS(1)
    wg := sync.WaitGroup{}
    wg.Add(20)
    for i := 0; i < 10; i++ {
        go func() {
            fmt.Println("A: ", i) // 全部只能打印出 A 10
            wg.Done()
        }()
    }
    for i := 0; i < 10; i++ {
        go func(i int) {
            fmt.Println("B: ", i)
            wg.Done()
        }(i)
    }
    wg.Wait()  // 两个 for 中的顺序也是交叉乱的
}

如上题中输出结果:

B  9
A  10
A  10
A  10
A  10
A  10
A  10
A  10
A  10
A  10
A  10
B  0
B  1
B  2
B  3
B  4
B  5
B  6
B  7
B  8

结构分析如下:

  1. goroutine的调度问题是无序的。
  2. 两个for前后顺序不定。
  3. A:均为输出10
  4. B:从0~9输出(顺序不定)。

主要发生的原因:

  1. goroutine本身的调度问题
  2. 引用了闭关
  3. for 的循环很快就跑完了,由于 goroutine 是异步执行的,当for跑完时都不一定开始执行,而当它开始执行行读取闭关共有的变量i就始终都是最后一个值(此处的10).
  4. 只要将当前的变量值传入到go func 中即可解决。

模拟项目中代码(最终简化版)

由于项目中的架构过于复杂,业务代码也较多,所以我将其抽象出来再简化一下,就如下代码这样:

func main() {
	instance := []string{"zxx", "xrr"}
	duration, err := time.ParseDuration("2s")
	if err != nil {
		log.Fatal(err)
	}

	for _, v := range instance {
		var item interface{}
		item = v + "$$$"
		fmt.Printf("【name】:%s 【time】 :%s \n ", v, item)
		go func(value string) {
			for {
				timer := time.NewTimer(duration)
				select {
				case <-timer.C:
				// 这里业务中由于是多租户操作,所以想要在这里让他输出 包含的内容要一致才行。
					fmt.Printf("go 【name】 :%s  【item】:%s \n", value, item)
				}
			}
		}(v)

	}

	time.Sleep(1 * time.Minute)
}

输出结果如下:

name】:zxxtime】 :zxx$$$
【name】:xrrtime】 :xrr$$$
goname】 :xrritem】:xrr$$$
// 前后包含元素一致!
// 错误情况: go 【name】 :xrr  【item】:zxx$$$
goname】 :zxxitem】:zxx$$$
goname】 :zxxitem】:zxx$$$
goname】 :xrritem】:xrr$$$
goname】 :zxxitem】:zxx$$$
goname】 :xrritem】:xrr$$$
goname】 :zxxitem】:zxx$$$
goname】 :xrritem】:xrr$$$

总结

其实很多面试题看上去短少的代码,但是里面的坑其实很多,尤其是如果在项目中出现,应该有意识的发现对应的知识点与问题,并且解决掉。

golang