如果您刚刚开始学习 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 高性能应用程序中使用它们。
如果您喜欢本教程或有任何意见/建议,请随时在下面的评论部分或旁边的建议部分告诉我!
- 登录 发表评论