sync.WaitGroup în Go

Așteaptă ca un grup de goroutine să termine: Add mărește contorul, Done îl scade, Wait blochează până ajunge la zero.

Sintaxă
var wg sync.WaitGroup
func (wg *WaitGroup) Add(delta int)
func (wg *WaitGroup) Done()
func (wg *WaitGroup) Wait()
func (wg *WaitGroup) Go(f func())   // Go 1.25

Ce face sync.WaitGroup

Funcția main nu așteaptă goroutine-urile pornite cu go: când main se termină, programul se oprește, chiar dacă ele nu au terminat. sync.WaitGroup este un contor de sarcini: wg.Add(1) înainte de a porni fiecare goroutine, wg.Done() (de obicei cu defer) când goroutine-ul termină, iar wg.Wait() blochează până când contorul ajunge la zero.

Add se apelează înainte de go, nu în interiorul goroutine-ului; altfel Wait poate vedea contorul 0 și trece mai departe prea devreme. Din Go 1.25, wg.Go(f) face toți pașii deodată (vezi pagina dedicată).

WaitGroup doar așteaptă, nu adună rezultate și nu transmite erori. Rezultatele le pui într-un slice la poziția fiecărei sarcini (fiecare goroutine scrie doar în elementul lui) sau le trimiți pe un canal.

Parametri

Parametru Descriere
delta Cât adaugi la contor; de obicei 1, sau numărul de sarcini (wg.Add(len(lista))).

Ce returnează

Nimic. Dacă contorul devine negativ (mai multe Done decât Add), programul se oprește cu panică.

Exemple

Exemplul 1: Descărcări în paralel

Fiecare goroutine scrie doar în elementul lui din slice, deci nu e nevoie de mutex. Afișarea se face după Wait, în ordinea listei.

package main

import (
	"fmt"
	"strings"
	"sync"
	"time"
)

func descarca(fisier string) string {
	time.Sleep(time.Duration(len(fisier)) * 5 * time.Millisecond) // durate diferite
	return strings.ToUpper(fisier) + " (ok)"
}

func main() {
	fisiere := []string{"factura.pdf", "poza.jpg", "contract.docx", "notite.txt"}
	rezultate := make([]string, len(fisiere))

	var wg sync.WaitGroup
	for i, f := range fisiere {
		wg.Add(1)
		go func() {
			defer wg.Done()
			rezultate[i] = descarca(f)
		}()
	}
	wg.Wait()

	for _, r := range rezultate {
		fmt.Println(r)
	}
	fmt.Println("Toate cele", len(fisiere), "descărcări s-au terminat")
}

Rezultat

FACTURA.PDF (ok)
POZA.JPG (ok)
CONTRACT.DOCX (ok)
NOTITE.TXT (ok)
Toate cele 4 descărcări s-au terminat

Exemplul 2: Suma pe bucăți, cu Add(n) o singură dată

Împărțim numerele în 4 bucăți, fiecare goroutine calculează suma bucății lui.

package main

import (
	"fmt"
	"sync"
)

func main() {
	numere := make([]int, 1000)
	for i := range numere {
		numere[i] = i + 1
	}
	const bucati = 4
	sume := make([]int, bucati)
	marime := len(numere) / bucati

	var wg sync.WaitGroup
	wg.Add(bucati)
	for b := range bucati {
		go func() {
			defer wg.Done()
			for _, n := range numere[b*marime : (b+1)*marime] {
				sume[b] += n
			}
		}()
	}
	wg.Wait()

	total := 0
	for _, s := range sume {
		total += s
	}
	fmt.Println("Sume parțiale:", sume)
	fmt.Println("Total:", total)
}

Rezultat

Sume parțiale: [31375 93875 156375 218875]
Total: 500500

Exemplul 3: Fără WaitGroup: main nu așteaptă

Programul se termină înainte ca goroutine-urile să apuce să scrie ceva. Uneori apare o linie, alteori niciuna.

package main

import "fmt"

func main() {
	for i := 1; i <= 3; i++ {
		go fmt.Println("goroutine", i)
	}
	fmt.Println("main s-a terminat")
}

Rezultat posibil (diferă la fiecare rulare)

main s-a terminat

Greșeli frecvente

  • Apelezi wg.Add(1) în interiorul goroutine-ului. Wait poate rula înainte și trece de contorul 0; apelează Add înainte de go.
  • Transmiți WaitGroup unei funcții prin valoare (func lucru(wg sync.WaitGroup)). Done scade contorul copiei și Wait rămâne blocat; transmite *sync.WaitGroup.
  • Uiți Done când funcția iese cu eroare. Pune defer wg.Done() pe prima linie a goroutine-ului.

Comenzi înrudite

  • sync.WaitGroup.Go: Pornește funcția f într-un goroutine nou și o adaugă în WaitGroup; înlocuiește perechea Add(1) + defer Done().
  • sync.Mutex: Lacăt de excludere reciprocă: doar un goroutine pe rând poate rula codul dintre Lock() și Unlock().
  • sync/atomic.Int64: Un int64 cu operații atomice ca metode (Add, Load, Store, Swap); varianta recomandată pentru contoare între goroutine.
  • context.WithCancel: Creează un context copil și o funcție cancel; apelul cancel() anunță toate goroutine-urile care îl folosesc să se oprească.