在完成一个需求时,我发现有个函数是这样写的:
代码语言:go复制func test(names []string) {
for _, name := range names {
doSomething(name)
}
}
观察逻辑发现这个数组中的每个元素执行起来,其实没有必然的先后关系,完全可以并发执行,于是我改了一版代码:
代码语言:go复制func test(names []string) {
for _, name := range names {
go func() {
doSomething(name)
}
}
time.Sleep(time.Second)
}
执行后令我疑惑的事情发生了,假设names="Zhao", "Qian", "Sun",那么执行的结果里只有"Sun"的记录。
由于当时刚接触Golang,想了半天也不知道为什么。但是后来我自己想明白了,for循环属于主goroutine,主goroutine才不会管其他goroutine是否被执行了。而我的goroutine是在循环结束的时候才执行的,这个时候的name就一定已经是"Sun"了。
比如这段代码,接近100%的概率什么都不会打印出来,这就是因为主goroutine只会自顾自的执行自己的工作,结束后则退出,那个时候一切都结束了:
代码语言:go复制fun main() {
go fmt.Println("Hello")
}
接下来做了另外一版的修改,这样就很完美了:
代码语言:go复制func test(names []string) {
for _, name := range names {
go func(n string) {
doSomething(n)
}(name)
}
time.Sleep(time.Second)
}
将name作为一个参数传入闭包中。此时函数内的name不会受到外部影响,这样就可以执行出正确的结果了。
这里有一点需要注意,由于name是string类型的,属于非引用类型,在当做参数被传入的时候,是会将其复制一份传入的,此时的入参就成了完全独立的存在,不受外部影响。
不过这个代码实现给我的感觉依旧很愚蠢,因为加上了一个sleep方法。如果有一个name的执行时间(或者调用接口网络抖动)超过了1s,当然主goroutine还是不会等它执行完成就会退出,会导致一些不可预见的问题发生。
总不可能无限制的增加sleep时长来换取安全性。实际上这些names是可以协同等待的,等待所有的goroutine执行结束之后,一起退出。这种模式有点类似于Java的CountDownLatch,在Golang中可以借助sync.WaitGroup来实现。