面试官:在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。根据具体需求选择合适的工具,确保程序逻辑清晰且无泄露。

相关推荐
程序员爱钓鱼2 小时前
Go语言中的文件与IO:bufio 和 scanner
后端·google·go
mCell19 小时前
你可能在用错密码:服务端密码安全的真相与陷阱
后端·安全·go
梦兮林夕1 天前
Docker + Gin + Gorm Gen:现代 Go Web 开发高效数据库实践
数据库·go·gin
mCell1 天前
项目配置管理的进化之路:从混乱到工程化
后端·go·代码规范
江湖十年1 天前
在 Go 中如何使用有限状态机优雅解决程序中状态转换问题
后端·面试·go
DemonAvenger1 天前
Go微服务架构下内存优化策略
性能优化·架构·go
程序员爱钓鱼1 天前
Go语言中的文件与IO:文件读写
后端·google·go
程序员爱钓鱼2 天前
Go语言同步原语与数据竞争:数据竞争的检测工具
后端·google·go
白总Server2 天前
GaussDB 分布式数据库调优(架构到全链路优化)
java·网络·c++·架构·go·scala·数据库架构
hacker_LeeFei3 天前
Goland使用手册(1)
go