无法弄清楚 goroutine 中的数据竞争

Trouble figuring out data race in goroutine

我最近开始学习围棋,现在我已经为此努力了一段时间,但我想是时候寻求一些具体的帮助了。我的程序从 api 请求分页数据,因为大约有 160 页数据。似乎很好地使用了 goroutines,除了我有竞争条件而且我似乎无法弄清楚为什么。这可能是因为我是这门语言的新手,但我的印象是函数的参数在调用它的函数中作为数据的副本传递,除非它是指针。

据我所知,这应该是复制我的数据,这样我就可以在主函数中自由更改它,但我最终请求了一些页面多次,而其他页面只请求了一次。

我的main.go

package main

import (
    "bufio"
    "encoding/json"
    "log"
    "net/http"
    "net/url"
    "os"
    "strconv"
    "sync"

    "github.com/joho/godotenv"
)

func main() {
    err := godotenv.Load()
    if err != nil {
        log.Fatalln(err)
    }

    httpClient := &http.Client{}
    baseURL := "https://api.data.gov/ed/collegescorecard/v1/schools.json"

    filters := make(map[string]string)
    page := 0
    filters["school.degrees_awarded.predominant"] = "2,3"
    filters["fields"] = "id,school.name,school.city,2018.student.size,2017.student.size,2017.earnings.3_yrs_after_completion.overall_count_over_poverty_line,2016.repayment.3_yr_repayment.overall"
    filters["api_key"] = os.Getenv("API_KEY")

    outFile, err := os.Create("./out.txt")
    if err != nil {
        log.Fatalln(err)
    }
    writer := bufio.NewWriter(outFile)

    requestURL := getRequestURL(baseURL, filters)

    response := requestData(requestURL, httpClient)

    wg := sync.WaitGroup{}
    for (page+1)*response.Metadata.ResultsPerPage < response.Metadata.TotalResults {
        page++
        filters["page"] = strconv.Itoa(page)

        wg.Add(1)
        go func() {
            defer wg.Done()

            requestURL := getRequestURL(baseURL, filters)

            response := requestData(requestURL, httpClient)

            _, err = writer.WriteString(response.TextOutput())
            if err != nil {
                log.Fatalln(err)
            }
        }()

    }

    wg.Wait()

}

func getRequestURL(baseURL string, filters map[string]string) *url.URL {
    requestURL, err := url.Parse(baseURL)
    if err != nil {
        log.Fatalln(err)
    }

    query := requestURL.Query()
    for key, value := range filters {
        query.Set(key, value)
    }
    requestURL.RawQuery = query.Encode()

    return requestURL
}

func requestData(url *url.URL, httpClient *http.Client) CollegeScoreCardResponseDTO {
    request, _ := http.NewRequest(http.MethodGet, url.String(), nil)

    resp, err := httpClient.Do(request)
    if err != nil {
        log.Fatalln(err)
    }
    defer resp.Body.Close()

    var parsedResponse CollegeScoreCardResponseDTO
    err = json.NewDecoder(resp.Body).Decode(&parsedResponse)
    if err != nil {
        log.Fatalln(err)
    }

    return parsedResponse
}

我知道我将 运行 解决的另一个问题是以正确的顺序写入输出文件,但我相信使用通道告诉每个例程什么请求完成写入可以解决这个问题。如果我对此有误,我也将不胜感激有关如何处理该问题的任何建议。

提前致谢。

goroutines 不接收数据副本。当编译器检测到变量“转义”当前函数时,它会在堆上分配该变量。在这种情况下,filters 就是这样一个变量。 goroutine启动时,它访问的filters是和主线程一样的map。由于您在主线程中不断修改 filters 而没有锁定,因此无法保证 goroutine 看到的内容。

我建议您将 filters 保留为只读,通过复制 filters 中的所有项目在 goroutine 中创建一个新映射,并在 goroutine 中添加 "page"。您还必须小心传递 page 的副本:

go func(page int) {
   flt:=make(map[string]string)
   for k,v:=range filters {
     flt[k]=v
   }
   flt["page"]=strconv.Itoa(page)
   ...
} (page)