Menunggu satu goroutine selesai cukup dilakukan dengan satu channel konfirmasi. Namun ketika pekerjaan dipecah menjadi lima puluh goroutine — unduh lima puluh berkas, proses lima puluh baris — membuat lima puluh channel dan menerima satu per satu menjadi beban tersendiri. Package sync menyediakan sync.WaitGroup: satu counter tugas yang dinaikkan sebelum pekerjaan dimulai, diturunkan tiap goroutine selesai, dan diawasi oleh pemanggil sampai nol. Chapter ini membedah ketiga methodnya, pola pemakaian yang benar, jebakan penempatan Add, dan aturan penting bahwa WaitGroup tidak boleh dicopy.
Counter Tugas: Add, Done, dan Wait #
sync.WaitGroup hanya punya tiga method. Add(n) menambah counter sebesar n — biasanya sekali per goroutine dengan Add(1) atau sekaligus dengan Add(len(tugas)). Done() menurunkan counter tepat satu; method ini adalah pemanggilan singkat Add(-1). Wait() memblokir goroutine pemanggil sampai counter mencapai nol. Berkas waitgroup-demo.go memakai ketiganya dalam dua fase berurutan:
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var wg sync.WaitGroup
// fase 1: unduh empat berkas secara bersamaan
tugas := []string{"unduh A", "unduh B", "unduh C", "unduh D"}
status := make([]string, len(tugas))
wg.Add(len(tugas))
for i, nama := range tugas {
go func() {
defer wg.Done()
time.Sleep(20 * time.Millisecond) // simulasi unduhan
status[i] = nama + " berhasil"
}()
}
wg.Wait()
for _, s := range status {
fmt.Println(s)
}
// fase 2: verifikasi hasil, WaitGroup yang sama dipakai ulang
verifikasi := make([]string, 2)
wg.Add(2)
for i := range 2 {
go func() {
defer wg.Done()
time.Sleep(10 * time.Millisecond) // simulasi pemeriksaan
verifikasi[i] = fmt.Sprintf("verifikasi %d lolos", i+1)
}()
}
wg.Wait()
for _, v := range verifikasi {
fmt.Println(v)
}
fmt.Println("semua tugas selesai")
}Output yang Dihasilkan:
unduh A berhasil
unduh B berhasil
unduh C berhasil
unduh D berhasil
verifikasi 1 lolos
verifikasi 2 lolos
semua tugas selesaiMari kita telaah baris demi baris program di atas:
- Baris 10 (
var wg sync.WaitGroup): mendeklarasikan WaitGroup bernilai nol yang siap dipakai — tidak perlu konstruktor. - Baris 16 (
wg.Add(len(tugas))): counter dinaikkan menjadi 4 sebelum loop memulai goroutine mana pun; posisiAddyang mendahuluigoadalah syarat kebenaran pola ini. - Baris 17–23 (
for ... go func): empat goroutine unduhan dijalankan; variabel loopidannamaberlaku per iterasi (Go 1.22+) sehingga tiap goroutine memegang pasangan nilainya sendiri. - Baris 19 (
defer wg.Done()):Done()dijadwalkan lewatdeferdan pasti terpanggil saat goroutine keluar, apa pun jalannya — idiom standar yang membuat counter mustahil lupa diturunkan. - Baris 21 (
status[i] = ...): tiap goroutine menulis ke slot indeksnya sendiri; setelahWaitkembali, pembaruan seluruh slot dijamin sudah terjadi dan aman dibacamain. - Baris 25 (
wg.Wait()):maintidur sampai keempat goroutine memanggilDone; hanya setelah baris ini hasil dicetak terurut sesuai urutan slice. - Baris 32 (
wg.Add(2)): counter yang sudah nol dipakai ulang untuk kelompok tugas fase kedua — satu WaitGroup boleh melayani banyak gelombang tugas berurutan selamaAddberikutnya menungguWaitsebelumnya selesai.
Urutan output program ini stabil karena pencetakan seluruhnya dilakukan main setelah tiap Wait, bukan oleh goroutine yang berlomba; pola “kumpulkan dulu, cetak terakhir” inilah cara standar mendapat output deterministik dari kerja concurrent.
Add Sebelum go, Done Lewat defer #
Dua aturan penempatan menentukan benar tidaknya WaitGroup. Aturan pertama: Add harus dipanggil sebelum statement go, di sisi pemanggil. Aturan kedua: Done dipasang sebagai defer di baris pertama goroutine. Pelanggaran aturan pertama menghasilkan race nyata:
// POLA SALAH — jangan ditiru
var wg sync.WaitGroup
for range 3 {
go func() {
wg.Add(1) // terlambat: belum dihitung pemanggil
defer wg.Done()
}()
}
wg.Wait() // bisa lolos saat counter masih 0Pada pola salah di atas wg.Wait() bisa segera lanjut ketika counter masih nol, karena ketiga Add(1) berlomba dengan Wait — scheduler belum tentu mengangkat goroutine mana pun sebelum main sampai ke Wait. Akibatnya program keluar sebelum pekerjaan selesai, dan gejalanya muncul-kadang-hilang-kadang, ciri klasik race condition yang dibedah di chapter Race Condition
.
WaitGroup Tidak Boleh Dicopy #
WaitGroup menyimpan counter dan mutex internal, jadi menyalin nilainya memutus hubungan dengan counter aslinya. Selalu teruskan WaitGroup sebagai pointer saat dibawa ke fungsi:
func proses(wg *sync.WaitGroup, id int) {
defer wg.Done()
// ... kerja goroutine ...
}Pemakaian proses(wg, id) (nilai, bukan pointer) akan ditolak oleh pemeriksa go vet dengan laporan copylocks — salah satu alasan selalu menjalankan go vet sebelum menggabungkan kode. Aturan yang sama berlaku untuk semua tipe sync termasuk sync.Mutex pada chapter berikutnya.
WaitGroup atau Channel #
WaitGroup menjawab satu pertanyaan: “sudah selesai semua?”. Ia tidak mengangkut data. Ketika hasil kerja perlu mengalir, channel tetap alat utama; kedua alat justru sering dipakai bersama — goroutine menulis hasil ke channel, dan WaitGroup memastikan semua penulis sudah selesai sebelum channel ditutup. Untuk sekadar menunggu sekelompok kerja tuntas tanpa data yang diantar, WaitGroup lebih ringkas dan maksudnya lebih terbaca.
Referensi #
- Package sync — dokumentasi resmi seluruh primitif sinkronisasi.
- sync.WaitGroup — aturan pemakaian Add, Done, dan Wait.
- Introducing the Go Race Detector — The Go Blog — membedah race akibat Add di dalam goroutine.
WaitGroup menjaga penyelesaian pekerjaan, tetapi ada satu keluarga masalah yang lebih dalam: banyak goroutine yang benar-benar menulis ke satu memori bersama. Untuk itu Go menutup rangkaian alat dasar ini dengan sync.Mutex dan sync.RWMutex.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
waitgroup-demo.go
Unduh berkaspackage main
import (
"fmt"
"sync"
"time"
)
func main() {
var wg sync.WaitGroup
// fase 1: unduh empat berkas secara bersamaan
tugas := []string{"unduh A", "unduh B", "unduh C", "unduh D"}
status := make([]string, len(tugas))
wg.Add(len(tugas))
for i, nama := range tugas {
go func() {
defer wg.Done()
time.Sleep(20 * time.Millisecond) // simulasi unduhan
status[i] = nama + " berhasil"
}()
}
wg.Wait()
for _, s := range status {
fmt.Println(s)
}
// fase 2: verifikasi hasil, WaitGroup yang sama dipakai ulang
verifikasi := make([]string, 2)
wg.Add(2)
for i := range 2 {
go func() {
defer wg.Done()
time.Sleep(10 * time.Millisecond) // simulasi pemeriksaan
verifikasi[i] = fmt.Sprintf("verifikasi %d lolos", i+1)
}()
}
wg.Wait()
for _, v := range verifikasi {
fmt.Println(v)
}
fmt.Println("semua tugas selesai")
}