Goroutines en kanalen in Go: praktische gelijktijdigheid met het communiceren van opeenvolgende processen
Go implementeert communicerende sequentiële processen: ultralichte goroutines, kanalen als pijpen getypt voor veilige communicatie, selecteer voor multiplexing, WaitGroup voor synchronisatie en context.Context voor gepropageerde verwijdering - in de meeste gevallen allemaal zonder expliciete mutexen van gevallen.
Het Go CSP-model
Ga de omarmen Communiceren van sequentiële processen (CSP), een formeel model voorgesteld door Tony Hoare in 1978. Het fundamentele principe: in plaats van meerdere goroutines die lezen en schrijven naar dezelfde variabele (met mutex), communiceren goroutines via kanaal - getypte pijpen die het eigendom van gegevens overdragen.
De Go-runtime beheert de planning van goroutines op een pool van OS-threads (standaard zelfs
aan het aantal beschikbare kernen, gecontroleerd door GOMAXPROCS). Een goroutine is dat
automatisch opgeschort tijdens blokkeerbewerkingen (I/O, kanaalrecv), waardoor anderen worden toegestaan
goroutine om vooruit te komen.
Goroutine: Anatomie en levenscyclus
package main
import (
"fmt"
"time"
)
func worker(id int, done chan struct{}) {
fmt.Printf("Worker %d started\n", id)
time.Sleep(100 * time.Millisecond) // simula lavoro
fmt.Printf("Worker %d finished\n", id)
done <- struct{}{} // segnala completamento
}
func main() {
done := make(chan struct{}, 5) // channel bufferizzato per 5
// Lancia 5 goroutine concorrentemente
for i := 0; i < 5; i++ {
go worker(i, done) // "go" avvia la goroutine
}
// Aspetta che tutte completino
for i := 0; i < 5; i++ {
<-done // riceve dal channel (blocca se vuoto)
}
fmt.Println("All workers done")
}
// Costo di una goroutine: ~2-8KB stack (cresce dinamicamente fino a 1GB)
// vs ~1MB per un OS thread
// Go può avere milioni di goroutine attive contemporaneamente
Kanaal: Typen en patronen
Kanaal ongebufferd en gebufferd
// Channel unbuffered: sincronizzazione diretta
// Send blocca finché un receiver è pronto
ch := make(chan int) // unbuffered
go func() { ch <- 42 }() // blocca finché qualcuno riceve
v := <-ch // sblocca il sender
// Channel buffered: coda FIFO con capacità fissata
// Send blocca SOLO se il buffer è pieno
buffered := make(chan int, 10) // buffer da 10 elementi
buffered <- 1 // non blocca (buffer ha spazio)
buffered <- 2 // non blocca
v := <-buffered // riceve 1 (FIFO)
// Directional channel types: sicurezza a compile time
func producer(ch chan<- int) { // solo write
ch <- 42
}
func consumer(ch <-chan int) { // solo read
v := <-ch
fmt.Println(v)
}
Patroon: pijpleiding
// Pipeline: catena di goroutine connesse da channel
// Ogni fase legge dall'input channel e scrive sull'output channel
func generate(nums ...int) <-chan int {
out := make(chan int)
go func() {
for _, n := range nums {
out <- n
}
close(out) // IMPORTANTE: chiudi quando finisci
}()
return out
}
func square(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for n := range in { // range su channel riceve finché chiuso
out <- n * n
}
close(out)
}()
return out
}
func main() {
// Compone la pipeline
c := generate(2, 3, 4, 5)
out := square(c)
// Consuma l'output
for v := range out {
fmt.Println(v) // 4, 9, 16, 25
}
}
Selecteer: Multiplexen op meerdere kanalen
select is het krachtigste trefwoord in Ga voor de concurrentie: wacht even
meerdere kanalen tegelijkertijd en voert de kanaalklare case uit. Indien meerdere kanalen
ze zijn klaar, hij kiest willekeurig.
// Timeout pattern con select
import "time"
func fetchWithTimeout(url string, timeout time.Duration) (string, error) {
resultCh := make(chan string, 1)
errCh := make(chan error, 1)
go func() {
result, err := fetch(url)
if err != nil {
errCh <- err
return
}
resultCh <- result
}()
select {
case result := <-resultCh:
return result, nil
case err := <-errCh:
return "", err
case <-time.After(timeout):
return "", fmt.Errorf("timeout after %v", timeout)
}
}
// Fan-out/fan-in con select e done channel
func merge(channels ...<-chan int) <-chan int {
merged := make(chan int)
var wg sync.WaitGroup
multiplex := func(ch <-chan int) {
defer wg.Done()
for v := range ch {
merged <- v
}
}
wg.Add(len(channels))
for _, ch := range channels {
go multiplex(ch)
}
go func() {
wg.Wait()
close(merged)
}()
return merged
}
context.Context: Gepropageerde verwijdering
Het pakket context is Go's standaardmechanisme voor het propageren van verwijdering
via een keten van goroutines. Elke functie die I/O of lang werk doet, zou dit moeten accepteren
een context.Context als eerste parameter:
import (
"context"
"fmt"
"time"
)
// Funzione che rispetta la cancellazione
func longOperation(ctx context.Context, id int) error {
for i := 0; i < 10; i++ {
select {
case <-ctx.Done(): // controlla se il context è cancellato
return ctx.Err() // errore: context.Canceled o DeadlineExceeded
default:
fmt.Printf("Step %d/%d\n", i+1, 10)
time.Sleep(100 * time.Millisecond)
}
}
return nil
}
func main() {
// Context con timeout di 500ms
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancel() // SEMPRE defer cancel() per liberare le risorse
err := longOperation(ctx, 1)
if err != nil {
fmt.Printf("Cancelled: %v\n", err) // context deadline exceeded
}
}
// Context si propaga attraverso le chiamate:
// Handler HTTP -> Service -> Repository -> Database
// Se il client disconnette, il context si cancella e risale tutta la catena
WaitGroup: synchronisatiepatroon
import "sync"
func processAll(items []Item) {
var wg sync.WaitGroup
results := make([]Result, len(items))
for i, item := range items {
wg.Add(1)
go func(i int, item Item) { // passa i e item come parametri!
defer wg.Done()
results[i] = process(item) // scrivere indici diversi è safe
}(i, item)
}
wg.Wait() // blocca finché tutti Done() sono stati chiamati
fmt.Println(results)
}
// ERRORE COMUNE: closure su variabile del loop (prima di Go 1.22)
// In Go 1.22+ il loop variable è scoped per iterazione -- no problema
for _, item := range items {
go func() {
process(item) // Go 1.22+: safe. Go <1.22: BUG! usa go func(i Item) invece
}()
}
Rasdetector: racegegevens vinden
// Compila ed esegui con il race detector integrato
go run -race main.go
go test -race ./...
// Esempio di data race che il detector trova:
var counter int
func increment() {
counter++ // DATA RACE! lettura + scrittura non atomica
}
// go run -race stamperà:
// WARNING: DATA RACE
// Write at 0x... by goroutine 6:
// main.increment()
// Previous write at 0x... by goroutine 5:
// main.increment()
// Fix con atomic o mutex:
import "sync/atomic"
var atomicCounter int64
atomic.AddInt64(&atomicCounter, 1) // atomico, thread-safe
Goroutine-lek: de meest voorkomende bug in Go
Een goroutine die vastloopt op een ontvangstkanaal (niemand verzendt ooit) of ontvangt niet
het nooit verwijderen van de context is een routinelek. Verzamel geheugen en CPU. Altijd gebruiken
context voor lange handelingen, en zorg ervoor dat elk kanaal een afzender heeft
en een ontvanger, of gebruik gebufferde kanalen met expliciete opruiming.
Conclusies
Het gelijktijdigheidsmodel van Go is een van de meest effectieve voor backend-services: goroutine
ultralichte kanalen die door hun ontwerp raceomstandigheden voorkomen, select voor multiplexen
e context voor gepropageerde annulering. Het grootste deel van de concurrerende Go-code
het heeft geen expliciete mutexen nodig.
Volgend artikel gaat verder naar Python: asyncio.TaskGroup en gestructureerde concurrentie in Python 3.11+, dat eindelijk semantische veiligheid ter wereld brengt die vergelijkbaar is met Go's CSP asynchroon/wachten.







