前言
最近实现某个业务时,须要读取数据而后再异步解决;在 Go 中实现起来天然就比较简单,伪代码如下:
list := []*Demo{{"a"}, {"b"}}
for _, v := range list {go func() {fmt.Println("name="+v.Name)
}()}
type Demo struct {Name string}
<!–more–>
看似非常简单几行代码却和咱们的预期不符,打印之后输入的是:
name=b
name=b
并不是咱们预期的:
name=a
name=b
坑一
因为写 go 的资格尚浅、道行更是肤浅,这 bug
我硬是找了个把小时;刚开始还认为是数据源的问题,经验了好几轮自我狐疑。总之过程先不表,先看看如何修复这个问题。
首先第一种方法是应用长期变量:
list := []*Demo{{"a"}, {"b"}}
for _, v := range list {
temp:=v
go func() {fmt.Println("name="+temp.Name)
}()}
这样便可正确输入,其实从这种写法中也能看出问题的端倪。
在第一种没有应用长期变量时,主协程很快就运行结束,这时候打印的子协程可能还没运行;当开始运行的时候,这里的 v 曾经被最初一个赋值了。
所以这里打印的始终都是最初一个变量。
而应用长期变量会将以后遍历的值拷贝一份,天然就不会相互影响了。
当然除了长期变量也可应用闭包解决。
list := []*Demo{{"a"}, {"b"}}
for _, v := range list {go func(temp *Demo) {fmt.Println("name="+temp.Name)
}(v)
}
将参数通过闭包传递时,每个 goroutine
都会在本人的栈中寄存一份参数的拷贝,这样也能辨别了。
坑二
与之类似的还有第二个坑:
list2 := []Demo{{"a"}, {"b"}}
var alist []*Demo
for _, test := range list2 {alist = append(alist, &test)
}
fmt.Println(alist[0].Name, alist[1].Name)
这段代码与咱们预期不不符:
b b
但咱们稍加批改就能够了:
list2 := []Demo{{"a"}, {"b"}}
var alist []Demo
for _, test := range list2 {fmt.Printf("addr=%p\n", &test)
alist = append(alist, test)
}
fmt.Println(alist[0].Name, alist[1].Name)
addr=0xc000010240
addr=0xc000010240
a b
顺便打印了内存地址,其实从后果中大略就能猜到起因;每次遍历打印的内存地址都是雷同,所以如果咱们寄存的是指针,实质上存储的都是同一块内存地址的内容,所以值雷同。
而如果咱们只存储值,不存指针天然也不会有这个问题。
但如果想应用指针如何解决呢?
list2 := []Demo{{"a"}, {"b"}}
var alist []*Demo
for _, test := range list2 {
temp := test
//fmt.Printf("addr=%p\n", &test)
alist = append(alist, &temp)
}
fmt.Println(alist[0].Name, alist[1].Name)
也简略,同样的应用长期变量即可。
通过官网源码能够得悉,for range
只是语法糖,实质上也是 for 循环;因为每次都是对同一个对象遍历赋值,所以便会呈现这样的“乌龙”。
defer 的坑
for
循环 + defer
也是组合坑(尽管不举荐这么用),还是先来看个例子:
// demo1
func main() {a := []int{1, 2, 3}
for _, v := range a {defer fmt.Println(v)
}
}
// demo2
func main() {a := []int{1, 2, 3}
for _, v := range a {defer func() {fmt.Println(v)
}()}
}
别离输入:
//demo1
3
2
1
//demo2
3
3
3
demo1
的后果很好了解,defer
能够了解为将执行语句放入到栈中,所以出现的后果是先进后出。
而 demo2
中,因为是闭包,闭包对变量 v 持有的是援用,所以在最终提早执行时 v 曾经被最初一个值赋值,所以打印进去都是雷同的。
解决办法与上文相似,传入参数即可解决:
for _, v := range a {defer func(v int) {fmt.Println(v)
}(v)
}
这类细节问题日常开发大概率是碰不上的,最有可能遇到的就是面试了,所以多理解理解也没害处。
总结
相似于第一种状况在 for 循环中 goroutine
调用,我感觉 IDE
齐全是能够做到揭示的;比方 IDEA
中就把大部分认为可能发的谬误蕴含进去,期待后续 goland
的更新。
但其实这几种谬误官网博客曾经揭示过了。
https://github.com/golang/go/wiki/CommonMistakes#using-reference-to-loop-iterator-variable
只是大部分人预计都没去看过,这事之后我也得花工夫好好浏览下。