乐趣区

关于golang:简单的-for-循环也会踩的坑

前言

最近实现某个业务时,须要读取数据而后再异步解决;在 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
只是大部分人预计都没去看过,这事之后我也得花工夫好好浏览下。

退出移动版