踩过的在Golang for-loop使用goroutine的坑
背景
在学习Golang的过程中,看到《Go语言圣经》中说到“在大多数程序中,一个WEB服务器就足够了”,然后我就想着那我就是想多个WEB服务器要怎么做呢,所以顺手写了下面的代码:
package main
import (
"fmt"
"net/http"
)
func echoHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello World!\n")
}
func main() {
ports := []string{":8090", ":8091"}
for _, v := range ports {
go func() {
mux := http.NewServeMux()
mux.HandleFunc("/echo", echoHandler)
http.ListenAndServe(v, mux)
}()
}
select {}
}
然后发现,运行的时候并没有如我预想的那样绑定两个端口,启动两个HTTP服务,总是只绑定了8091端口。
原因
先看看Go的wiki上有类似的问题:Using goroutines on loop iterator variables
初学者可能会使用如下代码来并行处理:
for val := range values {
go val.MyMethod()
}
或者使用闭包:
for val := range values {
go func() {
fmt.Println(val)
}()
}
这里的问题在于val实际上是一个变量了切片中所有数据的单一变量,由于闭包只是绑定到val变量上,因此极有可能上面的代码运行的结果是所有goroutine都输出了切片的最后一个元素。这是因为很有可能当for-loop执行完之后,goroutine才开始执行,这个时候val的值是指向了切片中最后一个元素。
解决办法
对于一开始我们给出的代码,我们稍作修改,如下:
package main
import (
"fmt"
"net/http"
)
func echoHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello World!\n")
}
func main() {
ports := []string{":8090", ":8091"}
for _, v := range ports {
go func(port string) {
mux := http.NewServeMux()
mux.HandleFunc("/echo", echoHandler)
http.ListenAndServe(port, mux)
}(v)
}
select {}
}
这里将 v 作为一个参数传入goroutine中,每个 v 都会被独立计算并保存到goroutine的栈中,从而得到预期的结果。
另一种方法是在循环内定义新的变量,由于循环内定义的变量在循环遍历的过程中不共享,因此也可以达到同样的效果:
for i := range myslice {
val := myslice[i]
go func() {
fmt.Println(val)
}()
}