在 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 在完成时向 
 - 
优点:
- 灵活性高,可以携带数据(例如任务结果)。
 - 适合动态数量的 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 | 
需要错误处理和等待的现代应用 | 优雅、功能强大 | 需要额外依赖 | 
面试可能追问
- "为什么主协程不直接 sleep?" 
time.Sleep是固定延迟,无法准确等待任务完成,可能导致过早退出或不必要的等待。同步工具更可靠。 - "
WaitGroup和通道有什么区别?"WaitGroup是计数器机制,专注于等待;通道是通信机制,可以传递数据,但需要手动同步。 - "如何处理 goroutine 中的错误?" 可以用通道返回错误,或使用 
errgroup统一收集。 
总结
主协程等待其他协程的最常用方法是 sync.WaitGroup,简单高效。如果需要错误处理或取消功能,推荐 errgroup 或结合 context。根据具体需求选择合适的工具,确保程序逻辑清晰且无泄露。