go și goroutine în Go

Goroutine și canale Go 1.0 (WaitGroup.Go: Go 1.25)

Cuvântul go pornește o funcție într-o goroutine: un fir de execuție ușor, care rulează în paralel cu restul programului.

Sintaxă
go functie(argumente)
go func() { ... }()

var wg sync.WaitGroup
wg.Add(1)
go func() { defer wg.Done(); ... }()
wg.Wait()

Ce face go și goroutine

go f(x) pornește apelul f(x) într-o goroutine nouă și continuă imediat, fără să aștepte rezultatul. O goroutine este un fir de execuție foarte ușor, gestionat de Go: un program poate rula mii sau chiar milioane deodată. main rulează și el într-o goroutine; când main se termină, programul se oprește, chiar dacă alte goroutine nu au terminat.

De aceea trebuie să aștepți goroutinele pornite. Cel mai simplu, cu sync.WaitGroup: Add(1) înainte de pornire, Done() la final (de obicei cu defer) și Wait() în main. Din Go 1.25, wg.Go(func() { ... }) face toate trei pașii. Rezultatele se trimit înapoi prin canale sau se scriu în poziții separate ale unui slice.

Ordinea în care rulează goroutinele nu este garantată. Pentru rezultate deterministe, colectează-le și afișează-le la final, în ordinea dorită. Datele modificate de mai multe goroutine trebuie protejate cu sync.Mutex, cu sync/atomic sau trimise prin canale; altfel apar erori de tip „data race” (detectate cu go run -race).

Exemple

Exemplul 1: Calcul în paralel cu WaitGroup, rezultate în ordine

Fiecare goroutine scrie în propria poziție din rezultate, deci nu e nevoie de mutex; afișarea se face după Wait, în ordine.

package main

import (
    "fmt"
    "sync"
)

func sumaPana(n int) int {
    s := 0
    for i := 1; i <= n; i++ {
        s += i
    }
    return s
}

func main() {
    limite := []int{10, 100, 1000, 10000}
    rezultate := make([]int, len(limite))

    var wg sync.WaitGroup
    for i, n := range limite {
        wg.Add(1)
        go func() {
            defer wg.Done()
            rezultate[i] = sumaPana(n)
        }()
    }
    wg.Wait() // așteptăm toate goroutinele

    for i, n := range limite {
        fmt.Printf("suma 1..%d = %d\n", n, rezultate[i])
    }
}

Rezultat

suma 1..10 = 55
suma 1..100 = 5050
suma 1..1000 = 500500
suma 1..10000 = 50005000

Exemplul 2: Contor partajat protejat cu Mutex

100 de goroutine cresc același contor; Lock/Unlock evită pierderea actualizărilor. wg.Go (Go 1.25) face Add și Done automat.

package main

import (
    "fmt"
    "sync"
)

func main() {
    var (
        mu     sync.Mutex
        wg     sync.WaitGroup
        vizite int
    )
    for range 100 {
        wg.Go(func() {
            for range 10 {
                mu.Lock()
                vizite++
                mu.Unlock()
            }
        })
    }
    wg.Wait()
    fmt.Println("vizite totale:", vizite)
}

Rezultat

vizite totale: 1000

Exemplul 3: Rezultate trimise prin canal

Goroutinele termină în orice ordine; colectăm rezultatele în map, apoi le afișăm sortat.

package main

import (
    "fmt"
    "slices"
    "strings"
)

type rezultat struct {
    oras   string
    litere int
}

func main() {
    orase := []string{"Iași", "Cluj-Napoca", "Arad", "Timișoara"}
    canal := make(chan rezultat)

    for _, o := range orase {
        go func() {
            canal <- rezultat{o, len([]rune(strings.ReplaceAll(o, "-", "")))}
        }()
    }

    litere := map[string]int{}
    for range orase {
        r := <-canal
        litere[r.oras] = r.litere
    }
    slices.Sort(orase)
    for _, o := range orase {
        fmt.Printf("%-12s %d litere\n", o, litere[o])
    }
}

Rezultat

Arad         4 litere
Cluj-Napoca  10 litere
Iași         4 litere
Timișoara    9 litere

Greșeli frecvente

  • Pornirea goroutinelor fără să le aștepți: main se termină, iar ele sunt oprite înainte de a face ceva.
  • wg.Add(1) în interiorul goroutinei: Wait poate trece înainte de Add. Apelează Add înainte de go; go vet semnalează WaitGroup.Add called from inside new goroutine.
  • Afișarea direct din goroutine când ordinea contează: ordinea diferă de la o rulare la alta.
  • Copierea unui sync.WaitGroup sau sync.Mutex prin trimitere ca valoare: go vet dă passes lock by value. Trimite un pointer.

Comenzi înrudite

  • chan și canale (channels): Canalele (chan) transmit valori între goroutine în siguranță și le sincronizează; se creează cu make.
  • select: Așteaptă mai multe operații pe canale deodată și o execută pe prima care este gata; cu default nu blochează.
  • close: Închide un canal: anunță că nu se mai trimit valori; range se oprește, iar primirea dă ok == false.
  • closures (funcții anonime): Funcții anonime care „țin minte” variabilele din jurul lor și le pot citi și modifica după ce funcția exterioară s-a terminat.
  • panic și recover: panic oprește execuția normală pentru erori grave; recover, apelat într-un defer, poate opri panic-ul.