WaitGroup


🚦 1. WaitGroup là gì?

sync.WaitGroup là một công cụ đồng bộ hoá trong Go, giúp đợi cho một nhóm goroutine hoàn thành công việc trước khi tiếp tục.

Cực kỳ hữu ích khi bạn fire nhiều goroutine song song và cần biết khi nào tất cả xong.


🛠 2. Cách dùng cơ bản

var wg sync.WaitGroup

for i := 0; i < 5; i++ {
    wg.Add(1) // tăng counter lên 1
    go func(i int) {
        defer wg.Done() // giảm counter đi 1 khi xong
        fmt.Println("Goroutine", i)
    }(i)
}

wg.Wait() // đợi tất cả goroutines xong
fmt.Println("All goroutines done.")

Ghi nhớ:

  • Add(n) = báo là có n công việc sẽ đến

  • Done() = báo là 1 công việc đã xong

  • Wait() = block cho đến khi counter về 0


⚠️ 3. Lỗi thường gặp

❌ Quên Done() → Deadlock

wg.Add(1)
go func() {
    // quên defer wg.Done()
}()
wg.Wait() // sẽ block mãi mãi

❌ Gọi Add() sau khi Wait() đã bắt đầu

Không thread-safe nếu gọi Add() sau khi Wait() đang chạy → race condition

✅ Cách đúng: Gọi toàn bộ Add() trước khi gọi Wait().


🧠 4. Sử dụng WaitGroup kết hợp với xử lý lỗi (pattern hay dùng)

Bạn có thể kết hợp với channel để collect error:

var wg sync.WaitGroup
errCh := make(chan error, 3) // buffered để không bị block

for _, task := range tasks {
    wg.Add(1)
    go func(t Task) {
        defer wg.Done()
        if err := t.Do(); err != nil {
            errCh <- err
        }
    }(task)
}

wg.Wait()
close(errCh)

for err := range errCh {
    fmt.Println("Error:", err)
}

📦 5. Best Practices từ Senior Dev

Best Practice
Giải thích

✅ Dùng defer wg.Done() ngay đầu goroutine

Để tránh quên gọi Done() khi panic

✅ Gọi Add(n) trước khi chạy goroutines

Đảm bảo tính nhất quán

🚫 Không gọi Wait() trong goroutine

Có thể gây deadlock nếu không cẩn thận

✅ Dùng sync.Once kết hợp khi cần chỉ gọi 1 lần

Trong trường hợp cần shutdown, hoặc init


🧪 6. Use-case thực tế (API Parallel Requests)

Ví dụ gọi song song nhiều API và chờ tất cả hoàn thành:

func fetchAll(urls []string) {
    var wg sync.WaitGroup
    for _, url := range urls {
        wg.Add(1)
        go func(u string) {
            defer wg.Done()
            resp, err := http.Get(u)
            if err != nil {
                log.Println("Error:", err)
                return
            }
            defer resp.Body.Close()
            body, _ := io.ReadAll(resp.Body)
            fmt.Println("Fetched:", u, "Length:", len(body))
        }(url)
    }
    wg.Wait()
    fmt.Println("All done.")
}

🚀 7. Bonus: Tự wrap WaitGroup thành TaskPool

Nếu bạn thích code gọn hơn:

type TaskGroup struct {
    wg sync.WaitGroup
}

func (tg *TaskGroup) Go(fn func()) {
    tg.wg.Add(1)
    go func() {
        defer tg.wg.Done()
        fn()
    }()
}

func (tg *TaskGroup) Wait() {
    tg.wg.Wait()
}

Dùng:

var tg TaskGroup

tg.Go(func() {
    doSomething()
})
tg.Go(func() {
    doAnotherThing()
})
tg.Wait()

🔚 Tổng kết

  • sync.WaitGroup là một công cụ rất mạnh khi làm việc với goroutine.

  • Là Senior, bạn cần hiểu kỹ lifecycle, tránh race condition, và dùng kết hợp channel, context, errgroup (khi cần).

  • Go’s concurrency dễ viết nhưng khó debug – nên cần cấu trúc và thói quen viết code rõ ràng, clean.

Last updated