Go 并发编程模式与示例

Posted on Mar 8, 2024

goroutine + channel 是 Go 并发的骨架,但真正的工程实践需要组合模式。


sync.WaitGroup — 等待一组 goroutine

func main() {
    var wg sync.WaitGroup

    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            fmt.Printf("worker %d done\n", id)
        }(i) // 注意传参,避免闭包捕获循环变量
    }

    wg.Wait()
    fmt.Println("all done")
}

常见错误wg.Add(1) 放在 go func 里面,可能导致 Add 还没执行 Wait 就返回了。

errgroup — 一组 goroutine 任一报错则取消

import "golang.org/x/sync/errgroup"

func main() {
    g, ctx := errgroup.WithContext(context.Background())

    urls := []string{"https://a.com", "https://b.com", "https://c.com"}
    for _, url := range urls {
        url := url
        g.Go(func() error {
            resp, err := http.Get(url)
            if err != nil {
                return err
            }
            resp.Body.Close()
            return nil
        })
    }

    if err := g.Wait(); err != nil {
        log.Fatal(err)
    }
}

errgroup 还支持限制并发数:g.SetLimit(3)

sync.Once — 只执行一次

var (
    once sync.Once
    db   *sql.DB
)

func GetDB() *sql.DB {
    once.Do(func() {
        var err error
        db, err = sql.Open("mysql", dsn)
        if err != nil {
            panic(err)
        }
    })
    return db
}

sync.Pool — 对象复用,降低 GC 压力

var bufPool = sync.Pool{
    New: func() any {
        return make([]byte, 1024)
    },
}

func handler(w http.ResponseWriter, r *http.Request) {
    buf := bufPool.Get().([]byte)
    defer bufPool.Put(buf)

    // 使用 buf ...
    buf = buf[:0] // 重置
}

注意:Pool 中的对象随时可能被 GC 回收,不能假设 Put 进去就一定能 Get 到。

context — 传递超时、取消、元数据

func worker(ctx context.Context, id int) error {
    for {
        select {
        case <-ctx.Done():
            fmt.Printf("worker %d canceled: %v\n", id, ctx.Err())
            return ctx.Err()
        default:
            // do work
            time.Sleep(500 * time.Millisecond)
        }
    }
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()

    for i := 0; i < 3; i++ {
        go worker(ctx, i)
    }

    <-ctx.Done()
    fmt.Println("main exit:", ctx.Err())
}
context.Background()
  └── WithCancel()        → 手动取消
  └── WithTimeout()       → 超时自动取消
  └── WithDeadline()      → 到时间点自动取消
  └── WithValue()         → 携带 key-value(少用,适合 traceID 等横切关注点)

工作池(Worker Pool)

func workerPool() {
    jobs := make(chan int, 100)
    results := make(chan int, 100)

    // 启动 3 个 worker
    var wg sync.WaitGroup
    for w := 1; w <= 3; w++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            for j := range jobs {
                results <- j * 2
            }
        }(w)
    }

    // 发送任务
    for j := 1; j <= 10; j++ {
        jobs <- j
    }
    close(jobs)

    // 等待 worker 完成,关闭结果通道
    go func() {
        wg.Wait()
        close(results)
    }()

    // 收集结果
    for r := range results {
        fmt.Println(r)
    }
}

扇出扇入(Fan-out / Fan-in)

// 扇出:一个输入 channel 分发给多个 worker
func fanOut(in <-chan int, workers int) []<-chan int {
    channels := make([]<-chan int, workers)
    for i := 0; i < workers; i++ {
        ch := make(chan int)
        go func(out chan int) {
            defer close(out)
            for v := range in {
                out <- v * v
            }
        }(ch)
        channels[i] = ch
    }
    return channels
}

// 扇入:多个 channel 合并到一个
func fanIn(channels ...<-chan int) <-chan int {
    out := make(chan int)
    var wg sync.WaitGroup
    for _, ch := range channels {
        wg.Add(1)
        go func(c <-chan int) {
            defer wg.Done()
            for v := range c {
                out <- v
            }
        }(ch)
    }
    go func() {
        wg.Wait()
        close(out)
    }()
    return out
}

func main() {
    in := make(chan int)
    go func() {
        for i := 1; i <= 10; i++ {
            in <- i
        }
        close(in)
    }()

    workers := fanOut(in, 4)
    out := fanIn(workers...)
    for v := range out {
        fmt.Println(v)
    }
}

Mutex vs Channel 的选择

规则:如果你的 goroutine 是在「管理状态」,用 mutex;如果是在「协调通信」,用 channel。

// 状态管理 → sync.Mutex
type Counter struct {
    mu    sync.Mutex
    count int
}
func (c *Counter) Inc() { c.mu.Lock(); c.count++; c.mu.Unlock() }

// 协调通信 → channel
done := make(chan struct{})
go func() { /* work */ close(done) }()
<-done

总结

模式场景
WaitGroup等待多个 goroutine 完成
errgroup任意错误即取消,限制并发
sync.Once单例、初始化
sync.Pool对象复用,降低 GC
context超时、取消、值传递
工作池限制并发、任务队列
扇出扇入并行处理 + 聚合