面试官:在go语言中,主协程如何等待其余协程完毕再操作?

在 Go 语言中,主协程(main goroutine)需要等待其他 goroutine 完成任务后再继续执行或退出程序,这是一个常见的并发同步需求。Go 提供了几种机制来实现这一点,具体取决于场景和需求。

方法 1:使用 sync.WaitGroup

sync.WaitGroup 是 Go 中最常用的同步工具,用于等待一组 goroutine 完成任务。它通过计数器机制工作,非常适合主协程等待多个子协程的情况。

示例代码

go 复制代码
package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup

    // 启动 3 个 goroutine
    for i := 1; i <= 3; i++ {
        wg.Add(1) // 计数器加 1
        go func(id int) {
            defer wg.Done() // 任务完成后计数器减 1
            fmt.Printf("Goroutine %d is running\n", id)
        }(i)
    }

    wg.Wait() // 主协程等待所有 goroutine 完成
    fmt.Println("All goroutines finished")
}

输出(顺序可能不同):

sql 复制代码
Goroutine 1 is running
Goroutine 2 is running
Goroutine 3 is running
All goroutines finished

工作原理

wg.Add(n):增加计数器,表示需要等待的 goroutine 数量。

wg.Done():每个 goroutine 完成时调用,计数器减 1。

wg.Wait():主协程阻塞,直到计数器归零。

优点: 简单易用,适合固定数量的 goroutine。无需额外通道,性能开销低。


方法 2:使用通道(Channel)

通过通道传递信号,主协程可以等待所有 goroutine 发送完成信号。这种方法更灵活,但通常比 WaitGroup 稍复杂。

示例代码

go 复制代码
package main

import "fmt"

func main() {
    done := make(chan struct{}) // 用于通知完成的信号通道
    numGoroutines := 3

    for i := 1; i <= numGoroutines; i++ {
        go func(id int) {
            fmt.Printf("Goroutine %d is running\n", id)
            done <- struct{}{} // 任务完成后发送信号
        }(i)
    }

    // 等待所有 goroutine 完成
    for i := 0; i < numGoroutines; i++ {
        <-done // 接收信号
    }
    fmt.Println("All goroutines finished")
}
  • 输出 (类似 WaitGroup 示例)。

  • 工作原理

    • 每个 goroutine 在完成时向 done 通道发送一个信号。
    • 主协程通过接收指定次数的信号来确认所有任务完成。
  • 优点

    • 灵活性高,可以携带数据(例如任务结果)。
    • 适合动态数量的 goroutine。
  • 缺点

    • 需要手动管理接收次数,代码稍显繁琐。

方法 3:结合 context 控制退出

使用 context.Context 可以优雅地控制 goroutine 的退出,并让主协程等待所有任务完成。这种方法特别适合需要取消或超时的场景。

示例代码

go 复制代码
package main

import (
    "context"
    "fmt"
    "sync"
)

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    var wg sync.WaitGroup

    for i := 1; i <= 3; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            select {
            case <-ctx.Done():
                fmt.Printf("Goroutine %d cancelled\n", id)
                return
            default:
                fmt.Printf("Goroutine %d is running\n", id)
            }
        }(i)
    }

    // 模拟任务完成
    cancel()       // 发送取消信号
    wg.Wait()      // 等待所有 goroutine 退出
    fmt.Println("All goroutines finished")
}

输出(可能因取消时机不同而变):

sql 复制代码
Goroutine 1 is running
Goroutine 2 is running
Goroutine 3 is running
All goroutines finished
  • 工作原理

    • context 用于通知 goroutine 退出。
    • WaitGroup 确保主协程等待所有 goroutine 完成。
  • 优点

    • 支持取消和超时,适合复杂并发场景。
  • 缺点

    • 代码复杂度稍高。

方法 4:使用 errgroup(推荐)

golang.org/x/sync/errgroup 是一个高级工具,结合了 WaitGroup 的等待功能和错误处理,特别适合需要等待一组任务并处理错误的情况。

示例代码

go 复制代码
package main

import (
    "fmt"
    "golang.org/x/sync/errgroup"
)

func main() {
    var g errgroup.Group

    for i := 1; i <= 3; i++ {
        id := i
        g.Go(func() error {
            fmt.Printf("Goroutine %d is running\n", id)
            return nil // 无错误
        })
    }

    if err := g.Wait(); err != nil {
        fmt.Println("Error:", err)
    } else {
        fmt.Println("All goroutines finished")
    }
}

输出

sql 复制代码
Goroutine 1 is running
Goroutine 2 is running
Goroutine 3 is running
All goroutines finished
  • 工作原理

    • g.Go() 启动一个 goroutine,并将其加入等待组。
    • g.Wait() 等待所有 goroutine 完成,并返回第一个非空错误(如果有)。
  • 优点

    • 简单优雅,支持错误传播。
    • 内置上下文支持(可通过 errgroup.WithContext)。
  • 安装

    • 需要 go get golang.org/x/sync/errgroup

选择哪种方法?

方法 适用场景 优点 缺点
sync.WaitGroup 固定数量的简单任务 简单高效 不支持错误或取消
通道 动态任务或需要传递结果 灵活性高 手动管理复杂
context 需要取消或超时的复杂场景 支持取消和超时 代码稍复杂
errgroup 需要错误处理和等待的现代应用 优雅、功能强大 需要额外依赖

面试可能追问

  1. "为什么主协程不直接 sleep?" time.Sleep 是固定延迟,无法准确等待任务完成,可能导致过早退出或不必要的等待。同步工具更可靠。
  2. "WaitGroup 和通道有什么区别?" WaitGroup 是计数器机制,专注于等待;通道是通信机制,可以传递数据,但需要手动同步。
  3. "如何处理 goroutine 中的错误?" 可以用通道返回错误,或使用 errgroup 统一收集。

总结

主协程等待其他协程的最常用方法是 sync.WaitGroup,简单高效。如果需要错误处理或取消功能,推荐 errgroup 或结合 context。根据具体需求选择合适的工具,确保程序逻辑清晰且无泄露。

相关推荐
叹人间,美中不足今方信6 小时前
gRPC服务发现
rpc·go·服务发现
Code季风6 小时前
将 gRPC 服务注册到 Consul:从配置到服务发现的完整实践(上)
数据库·微服务·go·json·服务发现·consul
Code季风10 小时前
微服务分布式配置中心:Gin Web 服务层与 gRPC 服务层集成 Nacos 实战
分布式·微服务·rpc·架构·go·gin·consul
考虑考虑12 小时前
go中的Map
后端·程序员·go
DemonAvenger15 小时前
Go中UDP编程:实战指南与使用场景
网络协议·架构·go
活椰拿铜15 小时前
Go实现超时控制
go
程序员爱钓鱼16 小时前
Go项目上线部署最佳实践:Docker容器化从入门到进阶
后端·google·go
Joker-01111 天前
牛客周赛Round 99(Go语言)
go·牛客周赛
Code季风2 天前
Gin Web 层集成 Viper 配置文件和 Zap 日志文件指南(下)
前端·微服务·架构·go·gin
Code季风2 天前
Gin Web 服务集成 Consul:从服务注册到服务发现实践指南(下)
java·前端·微服务·架构·go·gin·consul