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

相关推荐
addaduvyhup2 小时前
从 Java 的 Spring Boot MVC 转向 Go 语言开发的差异变化
java·spring boot·go·mvc
<e^πi+1=0>3 小时前
playwright-go实战:自动化登录测试
go·playwright
X_PENG5 小时前
【golang】是否复用buffer?分级缓冲池实现&slice底层原理
go
forever2313 小时前
go分布式master,worker服务,配合consul实现服务自动发现
go
程序员爱钓鱼13 小时前
Go 语言实用工具:如何高效解压 ZIP 文件
前端·后端·go
孔令飞13 小时前
简单粗暴:如何写一篇高质量的技术文章?
人工智能·云原生·go
zhuyasen1 天前
高性能缓存:使用 Redis 和本地内存缓存实战示例
redis·缓存·go
洛卡卡了1 天前
Gin 框架学习实录 · 第6篇:构建通用响应模块(统一结构体 + 错误码 + 分页封装)
go
洛卡卡了1 天前
Gin 框架学习实录 · 第5篇:用户模块增删改查 + 分页查询接口
go