在进行敏捷开发项目中,同事写出来一些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
结构分析如下:
- goroutine的调度问题是无序的。
- 两个for前后顺序不定。
- A:均为输出10
- B:从0~9输出(顺序不定)。
主要发生的原因:
- goroutine本身的调度问题
- 引用了闭关
- for 的循环很快就跑完了,由于 goroutine 是异步执行的,当for跑完时都不一定开始执行,而当它开始执行行读取闭关共有的变量i就始终都是最后一个值(此处的10).
- 只要将当前的变量值传入到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】:zxx 【time】 :zxx$$$
【name】:xrr 【time】 :xrr$$$
go 【name】 :xrr 【item】:xrr$$$
// 前后包含元素一致!
// 错误情况: go 【name】 :xrr 【item】:zxx$$$
go 【name】 :zxx 【item】:zxx$$$
go 【name】 :zxx 【item】:zxx$$$
go 【name】 :xrr 【item】:xrr$$$
go 【name】 :zxx 【item】:zxx$$$
go 【name】 :xrr 【item】:xrr$$$
go 【name】 :zxx 【item】:zxx$$$
go 【name】 :xrr 【item】:xrr$$$
总结
其实很多面试题看上去短少的代码,但是里面的坑其实很多,尤其是如果在项目中出现,应该有意识的发现对应的知识点与问题,并且解决掉。