Goroutines and Channels in Go: Praktická souběžnost s komunikací sekvenčních procesů
Go implementuje komunikující sekvenční procesy: ultralehké goroutiny, kanály jako potrubí zadejte pro zabezpečenou komunikaci, vyberte pro multiplexování, WaitGroup pro synchronizaci a kontext. Kontext pro propagované mazání – to vše bez explicitních mutexů případů.
Model Go CSP
Jdi obejmout Komunikující sekvenční procesy (CSP), formální model navrhl Tony Hoare v roce 1978. Základní princip: místo toho, abychom měli více goroutin které čtou a zapisují do stejné proměnné (s mutexem), goroutiny komunikují přes kanál — typované kanály, které přenášejí vlastnictví dat.
Runtime Go spravuje plánování goroutin ve fondu vláken OS (ve výchozím nastavení dokonce
na počet dostupných jader, řízený podle GOMAXPROCS). Gorutina je
automaticky pozastaveno během blokovacích operací (I/O, channel recv), což umožňuje ostatním
goroutine postoupit.
Goroutine: Anatomie a životní cyklus
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
Kanál: Typy a vzory
Kanál Unbuffered a Buffered
// 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)
}
Vzor: Potrubí
// 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
}
}
Vyberte: Multiplexování na více kanálech
select je nejúčinnějším klíčovým slovem v Go for the Competition: wait on
více kanálů současně a provede případ připravenosti kanálu. Pokud je více kanálů
jsou připraveni, vybírá náhodně.
// 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
}
kontext.Kontext: Propagované vymazání
Balíček context je standardní mechanismus Go pro šíření mazání
přes řetězec goroutinů. Každá funkce, která provádí I/O nebo dlouhou práci, by ji měla přijmout
a context.Context jako první parametr:
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: Vzor synchronizace
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
}()
}
Rasový detektor: Nalezení rasových dat
// 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 Leak: Nejběžnější chyba v Go
Gorutina, která se zasekne na přijímacím kanálu (nikdo nikdy neposílá) nebo nepřijímá
nikdy nesmazat kontext je rutinní únik. Akumulujte paměť a CPU. Vždy používejte
context pro dlouhé operace a ujistěte se, že každý kanál má odesílatele
a přijímač nebo použijte kanály s vyrovnávací pamětí s explicitním vyčištěním.
Závěry
Goův model souběžnosti patří mezi nejúčinnější pro backendové služby: goroutine
ultralehké kanály, které svým designem zabraňují závodním podmínkám, select pro multiplexování
e context pro propagované zrušení. Nejkonkurenceschopnější Go kód
nepotřebuje explicitní mutexy.
Další článek se přesune do Pythonu: asyncio.TaskGroup a strukturovaná soutěž v Pythonu 3.11+, který světu konečně přináší sémantickou bezpečnost srovnatelnou s CSP od Go asynchronní/čekání.







