踩过的在Golang for-loop使用goroutine的坑

作者: veaxen 分类: Golang 发布时间: 2018-11-29 19:32

背景

在学习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)
    }()
}

修改自:https://segmentfault.com/a/1190000010884717

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!

发表评论

电子邮件地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据