跳转到主要内容

如果您刚刚开始学习 Go 以及如何实现高并发、高性能应用程序,那么了解 WaitGroups 至关重要。

在本教程中,我们将介绍以下内容:

  • WaitGroups 是什么以及我们应该在什么时候使用它们
  • 使用 WaitGroups 的简单示例
  • WaitGroups 的真实示例

到此结束时,您应该对如何在您自己的并发 Go 应用程序中使用 WaitGroups 有一个坚实的掌握。

注意 - 本教程的完整代码可以在这里找到:TutorialEdge/go-waitgroup-tutorial

视频教程

https://youtu.be/0BPSR-W4GSY

了解等待组


让我们直接深入了解什么是 WaitGroup 以及它为我们解决了什么问题。

当您开始在 Go 中编写利用 goroutines 的应用程序时,您会遇到需要阻止代码库某些部分执行的场景,直到这些 goroutines 成功执行。

以这段代码为例:

package main

import "fmt"

func myFunc() {
    fmt.Println("Inside my goroutine")
}

func main() {
    fmt.Println("Hello World")
    go myFunc()
    fmt.Println("Finished Execution")
}

它在触发 goroutine 之前首先打印出 Hello World,最后打印出 Finished Execution。

然而,当你运行这个程序时,你应该注意到,当你运行这个程序时,它没有到达第 6 行,并且它从来没有真正打印出 Inside my goroutine。这是因为 main 函数实际上在 goroutine 有机会执行之前就终止了。

解决方案? - 等待组


WaitGroups 本质上允许我们通过阻塞直到 WaitGroup 中的任何 goroutine 成功执行来解决这个问题。

我们首先在 WaitGroup 上调用 .Add(1) 来设置我们想要等待的 goroutine 的数量,然后,我们在任何 goroutine 中调用 .Done() 来表示其执行结束。

注意 - 您需要确保在执行 goroutine 之前调用 .Add(1)。

一个简单的例子


现在我们已经介绍了基本理论,让我们看看如何通过使用 WaitGroups 来修复前面的示例:

package main

import (
    "fmt"
    "sync"
)

func myFunc(waitgroup *sync.WaitGroup) {
    fmt.Println("Inside my goroutine")
    waitgroup.Done()
}

func main() {
    fmt.Println("Hello World")

    var waitgroup sync.WaitGroup
    waitgroup.Add(1)
    go myFunc(&waitgroup)
    waitgroup.Wait()

    fmt.Println("Finished Execution")
}

如您所见,我们已经实例化了一个新的 sync.WaitGroup,然后调用 .Add(1) 方法,然后再尝试执行我们的 goroutine。

我们更新了函数以接收指向现有 sync.WaitGroup 的指针,然后在成功完成任务后调用 .Done() 方法。

最后,在第 19 行,我们调用 waitgroup.Wait() 来阻止 main() 函数的执行,直到 waitgroup 中的 goroutine 成功完成。

当我们运行这个程序时,我们现在应该看到以下输出:

$ go run main.go
Hello World
Inside my goroutine
Finished Execution

匿名函数


应该注意的是,如果我们愿意,我们可以使用匿名函数完成与上述相同的事情。如果 goroutine 本身不太复杂,这会更简洁,更容易阅读:

package main

import (
    "fmt"
    "sync"
)

func main() {
    fmt.Println("Hello World")

    var waitgroup sync.WaitGroup
    waitgroup.Add(1)
    go func() {
        fmt.Println("Inside my goroutine")
        waitgroup.Done()
    }()
    waitgroup.Wait()

    fmt.Println("Finished Execution")
}

同样,如果我们运行它,它会提供相同的结果:

$ go run main.go
Hello World
Inside my goroutine
Finished Execution


但是,当我们需要在函数中输入参数时,当我们这样做时,事情开始变得有点复杂。

例如,我们想将一个 URL 传递给我们的函数,我们必须像这样传递那个 URL:

go func(url string) {
  fmt.Println(url)
}(url)


如果您遇到此问题,请记住这一点。

一个“真实”世界的例子


在我的一个生产应用程序中,我的任务是创建一个与大量其他 API 交互的 API,并将结果汇​​总到一个响应中。

这些 API 调用中的每一个都需要大约 2-3 秒才能返回响应,并且由于我必须进行大量 API 调用,因此同步执行此操作是不可能的。

为了使这个端点可用,我必须使用 goroutine 并异步执行这些请求。

package main

import (
    "fmt"
    "log"
    "net/http"
)

var urls = []string{
    "https://google.com",
    "https://tutorialedge.net",
    "https://twitter.com",
}

func fetch(url string) {
    resp, err := http.Get(url)
    if err != nil {
        fmt.Println(err)
    }
    fmt.Println(resp.Status)
}

func homePage(w http.ResponseWriter, r *http.Request) {
    fmt.Println("HomePage Endpoint Hit")
    for _, url := range urls {
        go fetch(url)
    }
    fmt.Println("Returning Response")
    fmt.Fprintf(w, "All Responses Received")
}

func handleRequests() {
    http.HandleFunc("/", homePage)
    log.Fatal(http.ListenAndServe(":8081", nil))
}

func main() {
    handleRequests()
}

然而,当我第一次使用这种策略时,我注意到我对 API 端点的任何调用都在我的 goroutine 有机会完成并填充结果之前返回。

这是我了解 WaitGroups 并深入了解同步包的地方。

通过使用 WaitGroup,我可以有效地修复这种意外行为,并且仅在我的所有 goroutine 完成后才返回结果。

package main

import (
    "fmt"
    "log"
    "net/http"
    "sync"
)

var urls = []string{
    "https://google.com",
    "https://tutorialedge.net",
    "https://twitter.com",
}

func fetch(url string, wg *sync.WaitGroup) (string, error) {
    resp, err := http.Get(url)
    if err != nil {
        fmt.Println(err)
        return "", err
    }
    wg.Done()
    fmt.Println(resp.Status)
    return resp.Status, nil
}

func homePage(w http.ResponseWriter, r *http.Request) {
    fmt.Println("HomePage Endpoint Hit")
    var wg sync.WaitGroup

    for _, url := range urls {
        wg.Add(1)
        go fetch(url, &wg)
    }

    wg.Wait()
    fmt.Println("Returning Response")
    fmt.Fprintf(w, "Responses")
}

func handleRequests() {
    http.HandleFunc("/", homePage)
    log.Fatal(http.ListenAndServe(":8081", nil))
}

func main() {
    handleRequests()
}

现在我已将 WaitGroup 添加到此端点,它将对列出的所有 URL 执行 HTTP GET 请求,并且只有在完成后,才会向调用该特定端点的客户端返回响应。

$ go run wg.go
HomePage Endpoint Hit
200 OK
200 OK
200 OK
Returning Response


注意 - 解决此问题的方法不止一种,通过使用渠道可以实现类似的有效结果。 有关频道的更多信息 - Go Channels 教程

结论


在本教程中,我们学习了 WaitGroup 的基础知识,包括它们是什么以及我们如何在我们自己的 Go 高性能应用程序中使用它们。

如果您喜欢本教程或有任何意见/建议,请随时在下面的评论部分或旁边的建议部分告诉我!

 

文章链接