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:

go
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:

text
unduh A berhasil
unduh B berhasil
unduh C berhasil
unduh D berhasil
verifikasi 1 lolos
verifikasi 2 lolos
semua tugas selesai

Mari kita telaah baris demi baris program di atas:

  1. Baris 10 (var wg sync.WaitGroup): mendeklarasikan WaitGroup bernilai nol yang siap dipakai — tidak perlu konstruktor.
  2. Baris 16 (wg.Add(len(tugas))): counter dinaikkan menjadi 4 sebelum loop memulai goroutine mana pun; posisi Add yang mendahului go adalah syarat kebenaran pola ini.
  3. Baris 17–23 (for ... go func): empat goroutine unduhan dijalankan; variabel loop i dan nama berlaku per iterasi (Go 1.22+) sehingga tiap goroutine memegang pasangan nilainya sendiri.
  4. Baris 19 (defer wg.Done()): Done() dijadwalkan lewat defer dan pasti terpanggil saat goroutine keluar, apa pun jalannya — idiom standar yang membuat counter mustahil lupa diturunkan.
  5. Baris 21 (status[i] = ...): tiap goroutine menulis ke slot indeksnya sendiri; setelah Wait kembali, pembaruan seluruh slot dijamin sudah terjadi dan aman dibaca main.
  6. Baris 25 (wg.Wait()): main tidur sampai keempat goroutine memanggil Done; hanya setelah baris ini hasil dicetak terurut sesuai urutan slice.
  7. Baris 32 (wg.Add(2)): counter yang sudah nol dipakai ulang untuk kelompok tugas fase kedua — satu WaitGroup boleh melayani banyak gelombang tugas berurutan selama Add berikutnya menunggu Wait sebelumnya 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:

go
// 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 0

Pada 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:

go
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 #

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 berkas
go
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")
}