Skip to content

WaitGroup 等待组 - Golang并发编程面试题

WaitGroup是Go语言并发编程的重要同步原语,用于等待一组goroutine完成执行。本章深入解析WaitGroup的使用、原理和最佳实践。

📋 重点面试题

面试题 1:WaitGroup的基本用法和工作原理

难度级别:⭐⭐⭐
考察范围:并发同步/goroutine管理
技术标签sync.WaitGroup Add Done Wait

问题分析

WaitGroup是Go并发编程的基础同步工具,面试官经常考查其正确使用方法和常见陷阱。

详细解答

1. WaitGroup基本概念

点击查看完整代码实现
点击查看完整代码实现
go
package main

import (
    "fmt"
    "sync"
    "time"
)

// WaitGroup基本用法演示
func basicWaitGroupDemo() {
    var wg sync.WaitGroup
    
    // 启动3个goroutine
    for i := 0; i < 3; i++ {
        wg.Add(1) // 增加等待计数
        
        go func(id int) {
            defer wg.Done() // 完成时减少计数
            
            fmt.Printf("Goroutine %d 开始工作\n", id)
            time.Sleep(time.Duration(id) * time.Second)
            fmt.Printf("Goroutine %d 完成工作\n", id)
        }(i)
    }
    
    fmt.Println("等待所有goroutine完成...")
    wg.Wait() // 阻塞直到计数变为0
    fmt.Println("所有goroutine已完成")
}

:::

2. WaitGroup内部结构原理

点击查看完整代码实现
点击查看完整代码实现
点击查看完整代码实现
go
// WaitGroup的底层结构(简化版)
type WaitGroup struct {
    noCopy noCopy
    // state1包含计数器和等待者数量
    state1 [3]uint32
}

// WaitGroup的状态布局
// 64位系统上:
// |-------- counter (32bit) -------|-------- waiter (32bit) -------|
// |-------------- sema (32bit) -------------|

func explainWaitGroupStructure() {
    /*
    WaitGroup使用了一个巧妙的设计:
    1. counter: 跟踪还有多少个goroutine需要完成
    2. waiter: 跟踪有多少个goroutine在等待
    3. sema: 信号量,用于阻塞和唤醒等待的goroutine
    
    Add(delta): 增加counter的值
    Done(): 相当于Add(-1),减少counter的值
    Wait(): 如果counter > 0,增加waiter计数并阻塞
    */
    
    fmt.Println("WaitGroup内部使用信号量和原子操作实现同步")
}

::: :::

3. WaitGroup的正确使用模式

点击查看完整代码实现
点击查看完整代码实现
点击查看完整代码实现
go
// 正确的并发任务处理模式
func correctWaitGroupPattern() {
    var wg sync.WaitGroup
    tasks := []string{"task1", "task2", "task3", "task4"}
    
    for _, task := range tasks {
        wg.Add(1) // 在启动goroutine之前增加计数
        
        go func(taskName string) {
            defer wg.Done() // 使用defer确保Done一定会被调用
            
            fmt.Printf("处理任务: %s\n", taskName)
            // 模拟任务处理
            time.Sleep(time.Duration(len(taskName)) * 100 * time.Millisecond)
            fmt.Printf("任务 %s 完成\n", taskName)
        }(task) // 避免闭包变量捕获问题
    }
    
    wg.Wait()
    fmt.Println("所有任务处理完成")
}

::: :::

面试题 2:WaitGroup常见错误和陷阱

难度级别:⭐⭐⭐⭐
考察范围:并发编程陷阱/调试技巧
技术标签死锁 panic 竞态条件 闭包陷阱

问题分析

WaitGroup使用中有很多常见陷阱,理解这些陷阱有助于编写健壮的并发代码。

详细解答

1. 常见错误:Add和Done不匹配

点击查看完整代码实现
点击查看完整代码实现
点击查看完整代码实现
go
// 错误示例:Add和Done不匹配导致死锁
func wrongAddDonePattern() {
    var wg sync.WaitGroup
    
    // 错误1:Add在goroutine内部调用
    go func() {
        wg.Add(1) // ❌ 错误:可能导致Wait()在Add()之前调用
        defer wg.Done()
        time.Sleep(1 * time.Second)
        fmt.Println("任务完成")
    }()
    
    wg.Wait() // 可能在Add()之前就开始等待,导致死锁
}

// 正确示例:在启动goroutine之前调用Add
func correctAddPattern() {
    var wg sync.WaitGroup
    
    wg.Add(1) // ✅ 正确:在启动goroutine之前增加计数
    go func() {
        defer wg.Done()
        time.Sleep(1 * time.Second)
        fmt.Println("任务完成")
    }()
    
    wg.Wait()
}

::: :::

2. 常见错误:WaitGroup复制

点击查看完整代码实现
点击查看完整代码实现
点击查看完整代码实现
go
// WaitGroup不能复制的演示
func demonstrateNoCopy() {
    var wg sync.WaitGroup
    
    // ❌ 错误:传递WaitGroup的值会复制
    // 复制的WaitGroup状态独立,Wait()无法等待原始的goroutine
    go func(wg sync.WaitGroup) { // 错误:按值传递
        defer wg.Done()
        time.Sleep(1 * time.Second)
    }(wg)
    
    // ✅ 正确:传递指针
    wg.Add(1)
    go func(wg *sync.WaitGroup) {
        defer wg.Done()
        time.Sleep(1 * time.Second)
    }(&wg)
    
    wg.Wait()
}

// 使用结构体包含WaitGroup的正确方式
type TaskManager struct {
    wg sync.WaitGroup
}

func (tm *TaskManager) AddTask() {
    tm.wg.Add(1)
}

func (tm *TaskManager) TaskDone() {
    tm.wg.Done()
}

func (tm *TaskManager) WaitAll() {
    tm.wg.Wait()
}

::: :::

3. 常见错误:闭包变量捕获

点击查看完整代码实现
点击查看完整代码实现
点击查看完整代码实现
go
// 闭包变量捕获陷阱
func closureTrap() {
    var wg sync.WaitGroup
    
    // ❌ 错误:所有goroutine共享同一个变量i
    for i := 0; i < 3; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            fmt.Printf("处理任务 %d\n", i) // i的值可能都是3
        }()
    }
    
    // ✅ 正确:通过参数传递值
    for i := 0; i < 3; i++ {
        wg.Add(1)
        go func(taskId int) {
            defer wg.Done()
            fmt.Printf("处理任务 %d\n", taskId)
        }(i)
    }
    
    // ✅ 正确:在循环内声明新变量
    for i := 0; i < 3; i++ {
        wg.Add(1)
        i := i // 声明新的局部变量
        go func() {
            defer wg.Done()
            fmt.Printf("处理任务 %d\n", i)
        }()
    }
    
    wg.Wait()
}

::: :::

4. 常见错误:负计数导致panic

点击查看完整代码实现
点击查看完整代码实现
点击查看完整代码实现
go
func negativeCounterPanic() {
    var wg sync.WaitGroup
    
    // 场景1:Done()调用次数超过Add()
    wg.Add(2)
    
    go func() {
        defer wg.Done()
        time.Sleep(100 * time.Millisecond)
    }()
    
    go func() {
        defer wg.Done()
        defer wg.Done() // ❌ 错误:多调用了一次Done(),会panic
        time.Sleep(100 * time.Millisecond)
    }()
    
    wg.Wait()
}

// 安全的Done调用模式
func safeDonePattern() {
    var wg sync.WaitGroup
    
    wg.Add(1)
    go func() {
        defer func() {
            if r := recover(); r != nil {
                fmt.Printf("捕获到panic: %v\n", r)
            }
            wg.Done() // 确保Done只调用一次
        }()
        
        // 可能出现panic的代码
        // panic("模拟错误")
    }()
    
    wg.Wait()
}

::: :::

面试题 3:WaitGroup的高级应用场景

难度级别:⭐⭐⭐⭐
考察范围:实战应用/并发模式
技术标签worker池 限流 超时控制 错误收集

问题分析

在实际项目中,WaitGroup经常与其他并发原语结合使用,实现复杂的并发控制模式。

详细解答

1. Worker Pool模式

点击查看完整代码实现
点击查看完整代码实现
点击查看完整代码实现
go
import (
    "context"
    "fmt"
    "sync"
    "time"
)

// 工作任务定义
type Task struct {
    ID   int
    Data string
}

// Worker Pool使用WaitGroup
func workerPoolWithWaitGroup() {
    const numWorkers = 3
    const numTasks = 10
    
    tasks := make(chan Task, numTasks)
    var wg sync.WaitGroup
    
    // 启动worker goroutine
    for i := 0; i < numWorkers; i++ {
        wg.Add(1)
        go func(workerID int) {
            defer wg.Done()
            worker(workerID, tasks)
        }(i)
    }
    
    // 发送任务
    for i := 0; i < numTasks; i++ {
        tasks <- Task{ID: i, Data: fmt.Sprintf("task-%d", i)}
    }
    close(tasks) // 关闭任务通道,worker会自动退出
    
    wg.Wait() // 等待所有worker完成
    fmt.Println("所有任务处理完成")
}

func worker(id int, tasks <-chan Task) {
    for task := range tasks {
        fmt.Printf("Worker %d 处理任务 %d: %s\n", id, task.ID, task.Data)
        time.Sleep(200 * time.Millisecond) // 模拟处理时间
        fmt.Printf("Worker %d 完成任务 %d\n", id, task.ID)
    }
    fmt.Printf("Worker %d 退出\n", id)
}

::: :::

2. 带超时控制的WaitGroup

点击查看完整代码实现
点击查看完整代码实现
点击查看完整代码实现
go
// 实现带超时的WaitGroup
func waitGroupWithTimeout(wg *sync.WaitGroup, timeout time.Duration) bool {
    done := make(chan struct{})
    
    go func() {
        wg.Wait()
        close(done)
    }()
    
    select {
    case <-done:
        return true // 正常完成
    case <-time.After(timeout):
        return false // 超时
    }
}

func demonstrateTimeoutWaitGroup() {
    var wg sync.WaitGroup
    
    // 启动一个耗时的任务
    wg.Add(1)
    go func() {
        defer wg.Done()
        time.Sleep(3 * time.Second) // 模拟长时间任务
        fmt.Println("长时间任务完成")
    }()
    
    // 等待任务完成,但设置2秒超时
    if waitGroupWithTimeout(&wg, 2*time.Second) {
        fmt.Println("所有任务在超时前完成")
    } else {
        fmt.Println("任务未在指定时间内完成")
    }
}

::: :::

3. 错误收集模式

点击查看完整代码实现
点击查看完整代码实现
点击查看完整代码实现
go
import (
    "errors"
    "sync"
)

// 错误收集器
type ErrorCollector struct {
    mu     sync.Mutex
    errors []error
}

func (ec *ErrorCollector) AddError(err error) {
    if err != nil {
        ec.mu.Lock()
        ec.errors = append(ec.errors, err)
        ec.mu.Unlock()
    }
}

func (ec *ErrorCollector) GetErrors() []error {
    ec.mu.Lock()
    defer ec.mu.Unlock()
    return append([]error(nil), ec.errors...) // 返回副本
}

// 并发处理并收集错误
func concurrentProcessingWithErrorCollection() {
    var wg sync.WaitGroup
    collector := &ErrorCollector{}
    
    tasks := []int{1, 2, 3, 4, 5}
    
    for _, task := range tasks {
        wg.Add(1)
        go func(taskID int) {
            defer wg.Done()
            
            if err := processTask(taskID); err != nil {
                collector.AddError(fmt.Errorf("任务 %d 失败: %w", taskID, err))
            }
        }(task)
    }
    
    wg.Wait()
    
    // 处理收集到的错误
    if errs := collector.GetErrors(); len(errs) > 0 {
        fmt.Printf("处理过程中发生了 %d 个错误:\n", len(errs))
        for _, err := range errs {
            fmt.Printf("  - %v\n", err)
        }
    } else {
        fmt.Println("所有任务都成功完成")
    }
}

func processTask(taskID int) error {
    time.Sleep(100 * time.Millisecond)
    // 模拟某些任务失败
    if taskID%3 == 0 {
        return errors.New("模拟的处理错误")
    }
    fmt.Printf("任务 %d 完成\n", taskID)
    return nil
}

::: :::

4. 分批处理模式

点击查看完整代码实现
点击查看完整代码实现
点击查看完整代码实现
go
// 分批处理大量任务
func batchProcessingWithWaitGroup() {
    const batchSize = 5
    const totalTasks = 23
    
    allTasks := make([]Task, totalTasks)
    for i := 0; i < totalTasks; i++ {
        allTasks[i] = Task{ID: i, Data: fmt.Sprintf("batch-task-%d", i)}
    }
    
    // 按批处理任务
    for start := 0; start < len(allTasks); start += batchSize {
        end := start + batchSize
        if end > len(allTasks) {
            end = len(allTasks)
        }
        
        batch := allTasks[start:end]
        fmt.Printf("\n处理第 %d 批任务 (任务 %d-%d)\n", start/batchSize+1, start, end-1)
        
        var wg sync.WaitGroup
        for _, task := range batch {
            wg.Add(1)
            go func(t Task) {
                defer wg.Done()
                processBatchTask(t)
            }(task)
        }
        
        wg.Wait()
        fmt.Printf("第 %d 批任务完成\n", start/batchSize+1)
    }
    
    fmt.Println("所有批次处理完成")
}

func processBatchTask(task Task) {
    fmt.Printf("  处理任务 %d: %s\n", task.ID, task.Data)
    time.Sleep(50 * time.Millisecond)
}

::: :::

面试题 4:WaitGroup性能优化和最佳实践

难度级别:⭐⭐⭐⭐
考察范围:性能优化/生产实践
技术标签性能优化 内存对齐 false sharing best practices

问题分析

在高并发场景下,WaitGroup的使用方式会影响性能,了解优化技巧很重要。

详细解答

1. 避免频繁的Add/Done调用

点击查看完整代码实现
点击查看完整代码实现
点击查看完整代码实现
go
// 低效的方式:频繁调用Add
func inefficientWaitGroup() {
    var wg sync.WaitGroup
    
    for i := 0; i < 1000; i++ {
        wg.Add(1) // 每次循环都调用Add,可能产生竞争
        go func(id int) {
            defer wg.Done()
            // 快速任务
            fmt.Printf("快速任务 %d\n", id)
        }(i)
    }
    
    wg.Wait()
}

// 高效的方式:批量Add
func efficientWaitGroup() {
    var wg sync.WaitGroup
    taskCount := 1000
    
    wg.Add(taskCount) // 一次性添加所有计数
    
    for i := 0; i < taskCount; i++ {
        go func(id int) {
            defer wg.Done()
            fmt.Printf("快速任务 %d\n", id)
        }(i)
    }
    
    wg.Wait()
}

::: :::

2. 结合Context使用

点击查看完整代码实现
点击查看完整代码实现
点击查看完整代码实现
go
// WaitGroup与Context结合使用
func waitGroupWithContext(ctx context.Context) {
    var wg sync.WaitGroup
    
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            
            select {
            case <-ctx.Done():
                fmt.Printf("任务 %d 被取消: %v\n", id, ctx.Err())
                return
            case <-time.After(time.Duration(id) * time.Second):
                fmt.Printf("任务 %d 正常完成\n", id)
            }
        }(i)
    }
    
    // 启动一个goroutine来等待WaitGroup
    done := make(chan struct{})
    go func() {
        wg.Wait()
        close(done)
    }()
    
    // 等待完成或取消
    select {
    case <-done:
        fmt.Println("所有任务完成")
    case <-ctx.Done():
        fmt.Printf("操作被取消: %v\n", ctx.Err())
    }
}

func demonstrateContextWaitGroup() {
    // 创建可取消的context
    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()
    
    waitGroupWithContext(ctx)
}

::: :::

3. WaitGroup最佳实践总结

点击查看完整代码实现
点击查看完整代码实现
点击查看完整代码实现
go
// WaitGroup最佳实践示例
type SafeWaitGroupManager struct {
    wg sync.WaitGroup
    mu sync.RWMutex
}

func (swg *SafeWaitGroupManager) Add(delta int) {
    swg.mu.Lock()
    defer swg.mu.Unlock()
    swg.wg.Add(delta)
}

func (swg *SafeWaitGroupManager) Done() {
    swg.mu.RLock()
    defer swg.mu.RUnlock()
    swg.wg.Done()
}

func (swg *SafeWaitGroupManager) Wait() {
    swg.mu.RLock()
    defer swg.mu.RUnlock()
    swg.wg.Wait()
}

// 最佳实践要点
func waitGroupBestPractices() {
    fmt.Println("WaitGroup最佳实践:")
    fmt.Println("1. 在启动goroutine之前调用Add()")
    fmt.Println("2. 使用defer确保Done()被调用")
    fmt.Println("3. 不要复制WaitGroup,通过指针传递")
    fmt.Println("4. Add和Done的调用必须配对")
    fmt.Println("5. 避免在goroutine内部调用Add()")
    fmt.Println("6. 考虑使用Context进行取消控制")
    fmt.Println("7. 在高并发场景下批量调用Add()")
    fmt.Println("8. 结合其他同步原语实现复杂的并发控制")
}

::: :::

🎯 核心知识点总结

WaitGroup基础要点

  1. 基本操作: Add()增加计数,Done()减少计数,Wait()等待计数归零
  2. 内部实现: 使用原子操作和信号量实现高效同步
  3. 生命周期: 计数器从0开始,通过Add增加,Done减少,Wait在计数为0时返回
  4. 并发安全: WaitGroup的所有方法都是并发安全的

常见陷阱要点

  1. Add/Done不匹配: 会导致死锁或panic
  2. WaitGroup复制: 按值传递会导致状态不同步
  3. 闭包变量捕获: 循环中启动goroutine要注意变量作用域
  4. 负计数: Done()调用过多会导致panic

高级应用要点

  1. Worker Pool: 结合channel实现工作池模式
  2. 超时控制: 结合select和timer实现超时等待
  3. 错误收集: 并发任务的错误统一收集和处理
  4. 批量处理: 分批处理大量任务,控制并发度

性能优化要点

  1. 批量Add: 避免频繁的Add调用减少竞争
  2. Context结合: 支持取消和超时控制
  3. 内存对齐: 注意WaitGroup在结构体中的位置
  4. 适度并发: 控制goroutine数量避免过度并发

🔍 面试准备建议

  1. 掌握原理: 理解WaitGroup的内部实现和工作机制
  2. 识别陷阱: 熟悉常见错误模式和解决方案
  3. 实战练习: 通过编写并发程序加深理解
  4. 性能考虑: 了解在高并发场景下的性能优化技巧

正在精进