Menu

WaitGroup di Golang: Add, Done, Wait, dan Worker Pool

Cara sync.WaitGroup menunggu sekumpulan goroutine selesai: aturan Add, Done, dan Wait, kenapa harus dikirim sebagai pointer, cara mengumpulkan hasil dan error, serta worker pool yang dibangun di atasnya.

Halaman ini dilengkapi editor yang bisa dijalankan. Edit, jalankan, dan lihat output-nya seketika.

Pola dasarnya

sync.WaitGroup menghitung goroutine yang sedang berjalan. Add menaikkan hitungannya, Done menurunkannya, Wait memblokir sampai hitungannya nol.

Tiga download itu berjalan bersamaan, jadi program butuh sekitar 10 ms, bukan 30. Hasilnya dicetak sesuai urutan input karena setiap goroutine hanya menulis ke indeksnya sendiri di sizes, dan main baru membacanya setelah Wait.

Zero value dari WaitGroup sudah siap dipakai. Tidak perlu constructor.

Tiga aturan

Panggil Add sebelum go, bukan di dalam goroutine. Kalau goroutine memanggil Add sendiri, main bisa sampai di Wait sebelum ada goroutine yang mulai, melihat hitungan nol, lalu return padahal pekerjaannya belum dimulai. Kalau kamu sudah tahu jumlahnya sejak awal, wg.Add(len(files)) sekali sebelum perulangan hasilnya sama.

Panggil Done dengan defer di baris pertama goroutine. Goroutine yang return lebih awal karena error, atau panic, tetap menurunkan penghitungnya. Done yang terlupa membuat Wait memblokir selamanya. Kalau itu satu-satunya goroutine yang tersisa, runtime melaporkan fatal error: all goroutines are asleep dengan sync.WaitGroup.Wait di trace-nya.

Jangan pernah menyalin WaitGroup setelah pertama kali dipakai. Kirim *sync.WaitGroup ke fungsi, atau tangkap variabelnya lewat closure seperti di atas.

Mengirim WaitGroup ke fungsi

Kalau badan goroutine berupa fungsi bernama, kirim pointer-nya:

Dengan wg sync.WaitGroup sebagai parameter nilai, setiap worker akan memanggil Done pada salinannya sendiri dan main akan memblokir di Wait selamanya. go vet menangkapnya sebelum kamu menjalankan apa pun:

./main.go:8:24: worker passes lock by value: sync.WaitGroup contains sync.noCopy

Desain yang lebih rapi menjauhkan konkurensi dari worker sepenuhnya: biarkan ia menjadi fungsi biasa, dan lakukan pencatatan Add/Done di closure milik pemanggil. Dengan begitu worker mudah diuji dan dipanggil secara sinkron.

Penghitung negatif

Done sama dengan Add(-1). Kalau hitungannya turun di bawah nol, program akan panic:

Output-nya adalah recovered: sync: negative WaitGroup counter. Penyebab yang biasa adalah goroutine dengan defer wg.Done() yang juga memanggil wg.Done() secara eksplisit di salah satu jalurnya.

Mengumpulkan error

WaitGroup hanya menghitung. Untuk error, beri setiap goroutine slotnya sendiri dan periksa setelah Wait:

errors.Join (Go 1.20) melewati nilai nil dan mengembalikan nil jika semuanya nil, jadi fungsi ini menggabungkan "satu error per goroutine" tanpa pencatatan tambahan.

Kalau kamu ingin menghentikan pekerjaan yang tersisa begitu ada satu goroutine yang gagal, pakai golang.org/x/sync/errgroup. Ini adalah WaitGroup ditambah error pertama ditambah context yang dibatalkan saat terjadi kegagalan, dan g.SetLimit(n) membatasi konkurensinya. Package ini berada di luar pustaka standar, jadi tidak bisa dijalankan di editor halaman ini:

g, ctx := errgroup.WithContext(ctx)
for _, h := range hosts {
	g.Go(func() error { return checkCtx(ctx, h) })
}
if err := g.Wait(); err != nil {
	return err // the first error; ctx was cancelled for the others
}

Worker pool

Sejumlah goroutine tetap yang membaca job dari sebuah channel menjaga konkurensi tetap terbatas, tidak peduli berapa banyak job-nya. WaitGroup memberi tahu kapan semua worker selesai, dan saat itulah channel hasil boleh ditutup.

Urutan ketiga bagian ini penting:

  • main harus sedang menerima hasil selama worker berjalan. Kalau main langsung memanggil wg.Wait() sebelum membaca, worker akan terblokir saat mengirim ke results, tidak pernah sampai ke Done, dan semuanya mengalami deadlock. Itulah sebabnya Wait dijalankan di goroutine-nya sendiri.
  • close(results) hanya terjadi setelah Wait, jadi tidak ada worker yang bisa mengirim ke channel yang sudah ditutup.
  • Pengirim job juga berjalan di sebuah goroutine, jadi proses mengirim dan mengumpulkan berlangsung bersamaan.

Worker mana yang menangani job mana berubah di setiap run, jadi program mengurutkan berdasarkan job sebelum mencetak. Semua yang dicetaknya deterministik.

WaitGroup, channel, atau errgroup

KebutuhanPakai
Menunggu N goroutine, hasil di slot berindekssync.WaitGroup
Menunggu satu goroutinechannel done atau channel hasil itu sendiri
Hasil dialirkan begitu selesaichannel, ditutup setelah wg.Wait()
Menghentikan semuanya pada error pertamaerrgroup.WithContext
Menghentikan semuanya karena timeout atau dibatalkan pemanggilcontext.Context ditambah WaitGroup atau errgroup

Go 1.25 menambahkan wg.Go(func() { ... }), yang melakukan Add(1) dan Done yang di-defer untukmu. Kode untuk Go 1.24 dan sebelumnya, termasuk editor di halaman ini, memakai bentuk eksplisit seperti di atas.

Kesalahan umum

  • wg.Add(1) di dalam goroutine. Wait bisa return sebelum baris itu dijalankan.
  • Lupa Done saat return lebih awal. Selalu tulis defer wg.Done().
  • Mengirim WaitGroup sebagai nilai. Pakai pointer; go vet menandai penyalinannya.
  • Menunggu di goroutine yang sama yang harus mengosongkan channel. Pindahkan wg.Wait() beserta close ke goroutine terpisah.
  • Memakai ulang WaitGroup sebelum Wait sebelumnya return. Mulai siklus panggilan Add yang baru hanya setelah Wait selesai.

Pertanyaan yang sering diajukan

Bagaimana cara kerja sync.WaitGroup di Go?

WaitGroup adalah sebuah penghitung. wg.Add(n) menaikkannya, wg.Done() menurunkannya satu, dan wg.Wait() memblokir sampai nilainya nol. Panggil Add sebelum menjalankan setiap goroutine, defer wg.Done() di dalamnya, dan Wait di tempat kamu butuh semuanya sudah selesai.

Sebaiknya WaitGroup dikirim sebagai nilai atau pointer?

Sebagai pointer (*sync.WaitGroup), atau biarkan goroutine menangkapnya lewat closure. Salinan punya penghitungnya sendiri, jadi Done pada salinan tidak pernah sampai ke aslinya dan Wait memblokir selamanya. go vet melaporkan kesalahan ini sebagai "passes lock by value".

Apa penyebab "sync: negative WaitGroup counter"?

Panggilan Done lebih banyak daripada panggilan Add. Biasanya sebuah goroutine memanggil Done dua kali (sekali dengan defer dan sekali secara eksplisit), atau Add(1) terlewat di salah satu jalur. Program akan panic, karena penghitung itu tidak lagi bisa memberi informasi yang benar.

Bagaimana cara mendapatkan error dari goroutine yang dijalankan dengan WaitGroup?

WaitGroup tidak membawa hasil atau error. Beri setiap goroutine slotnya sendiri di sebuah slice error dan gabungkan setelah Wait (misalnya dengan errors.Join), atau pakai golang.org/x/sync/errgroup, yang Wait-nya mengembalikan error pertama dan bisa membatalkan goroutine lain lewat sebuah context.

Ilustrasi bahasa pemrograman di Coddy

Belajar coding dengan Coddy

Mulai