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.