Colloquio Tecnico Go: Goroutine, Channel e Concorrenza nel 2026
Domande di colloquio Go su goroutine, channel e concorrenza con esempi di codice. Preparazione completa per il colloquio tecnico Golang.

Le domande di colloquio Go su goroutine, channel e concorrenza rappresentano uno degli ostacoli tecnici più impegnativi per chi affronta un processo di selezione nel 2026. Padroneggiare questi concetti in profondità distingue gli sviluppatori Go senior da chi si limita a conoscere la sintassi di base. Questa guida copre le esatte go interview questions poste dai selezionatori, con esempi di codice di livello produttivo e il ragionamento alla base di ciascuna risposta.
I colloqui tecnici sulla concorrenza in Go si concentrano su tre aree: la gestione del ciclo di vita delle goroutine, la semantica dei channel (buffered vs unbuffered, tipi direzionali) e la composizione di pattern (fan-out/fan-in, worker pool, cancellazione tramite context). Memorizzare la sintassi non basta: il candidato deve dimostrare la capacità di ragionare su race condition e deadlock.
Fondamenti delle Goroutine: le Domande Ricorrenti
La prima fase di un colloquio tecnico Go verifica se il candidato comprende cosa sono realmente le goroutine, non soltanto come lanciarle.
D: Che cos'è una goroutine e in cosa differisce da un thread del sistema operativo?
Una goroutine è una funzione concorrente leggera gestita dallo scheduler del runtime Go, non dal sistema operativo. Il runtime Go multiplexa migliaia di goroutine su un numero ridotto di thread OS attraverso un modello di scheduling M:N (M goroutine mappate su N thread OS).
package main
import (
"fmt"
"runtime"
"sync"
)
func main() {
// Print the number of OS threads available
fmt.Println("GOMAXPROCS:", runtime.GOMAXPROCS(0))
var wg sync.WaitGroup
for i := 0; i < 10000; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
// Each goroutine starts with ~2-8KB stack
// OS threads typically start with 1-8MB
_ = id
}(i)
}
wg.Wait()
fmt.Println("All 10,000 goroutines completed")
}Differenze chiave da menzionare durante un colloquio: le goroutine partono con uno stack di 2-8KB che cresce dinamicamente, contro lo stack fisso di 1-8MB di un thread OS. Il cambio di contesto tra goroutine avviene in user space tramite lo scheduler di Go, evitando i costosi context switch a livello kernel. Questo rende pratico l'avvio di 100.000 goroutine simultanee, mentre altrettanti thread OS esaurirebbero le risorse di sistema.
D: Cosa succede se una goroutine va in panic?
Un panic non recuperato in qualsiasi goroutine provoca il crash dell'intero programma. A differenza delle eccezioni in Java o Python, un panic risale lo stack della goroutine in cui si verifica, non quello della goroutine che l'ha lanciata. L'unico modo per intercettarlo è utilizzare recover() all'interno di una funzione differita nella stessa goroutine.
package main
import "fmt"
func safeGo(fn func()) {
go func() {
defer func() {
if r := recover(); r != nil {
fmt.Println("recovered from panic:", r)
}
}()
fn() // execute the actual work
}()
}
func main() {
safeGo(func() {
panic("something went wrong")
})
// Give goroutine time to complete
select {}
}I selezionatori cercano la consapevolezza che nei servizi Go in produzione ogni lancio di goroutine viene avvolto in un pattern di recovery. Librerie come errgroup gestiscono questo aspetto in modo più elegante e strutturato.
Semantica dei Channel: Buffered, Unbuffered e Direzionali
Le domande sui channel rivelano se il candidato comprende realmente il modello di concorrenza di Go o se si limita a ripetere pattern memorizzati.
D: Qual è la differenza tra un channel buffered e uno unbuffered?
Un channel unbuffered (make(chan T)) richiede che mittente e destinatario siano pronti simultaneamente: l'invio blocca fino a quando un'altra goroutine riceve. Un channel buffered (make(chan T, n)) consente l'invio di fino a n valori senza blocco.
package main
import "fmt"
func main() {
// Unbuffered: send blocks until receive is ready
ch := make(chan string)
go func() {
ch <- "hello" // blocks here until main reads
}()
msg := <-ch
fmt.Println(msg)
// Buffered: send does not block until buffer is full
buf := make(chan int, 3)
buf <- 1 // does not block (buffer has space)
buf <- 2 // does not block
buf <- 3 // does not block
// buf <- 4 would block — buffer is full
fmt.Println(<-buf, <-buf, <-buf) // 1 2 3
}La domanda di approfondimento che segue frequentemente: "Quando si sceglie l'uno rispetto all'altro?" I channel unbuffered impongono la sincronizzazione ed è utile quando il mittente deve sapere che il destinatario ha elaborato il valore. I channel buffered disaccoppiano la temporizzazione tra mittente e destinatario, risultando ideali per code di lavoro o rate limiting in cui una certa flessibilità è accettabile.
D: Cosa succede quando si chiude un channel?
La chiusura di un channel segnala che non verranno inviati altri valori. Le operazioni di ricezione su un channel chiuso restituiscono immediatamente il valore zero del tipo. L'invio su un channel chiuso provoca un panic. Un ciclo range su un channel termina automaticamente alla chiusura del channel stesso.
package main
import "fmt"
func producer(ch chan<- int, count int) {
for i := 0; i < count; i++ {
ch <- i
}
close(ch) // signal: no more values
}
func main() {
ch := make(chan int, 5)
go producer(ch, 5)
// range exits automatically when channel closes
for val := range ch {
fmt.Println("received:", val)
}
// Reading from closed channel returns zero value + false
val, ok := <-ch
fmt.Printf("after close: val=%d, ok=%v\n", val, ok)
}Un punto critico: solo il mittente dovrebbe chiudere un channel, mai il destinatario. Chiudere un channel su cui un'altra goroutine sta ancora scrivendo provoca un panic immediato.
Pronto a superare i tuoi colloqui su Go?
Pratica con i nostri simulatori interattivi, flashcards e test tecnici.
L'istruzione Select: Multiplexing tra Channel
D: Come funziona select e cosa succede quando più casi sono pronti contemporaneamente?
L'istruzione select blocca fino a quando una delle sue operazioni su channel può procedere. Quando più casi sono pronti simultaneamente, Go ne seleziona uno in modo casuale, impedendo così la starvation di qualsiasi caso specifico.
package main
import (
"context"
"fmt"
"time"
)
func fetchFromAPI(ctx context.Context, url string) (string, error) {
resultCh := make(chan string, 1)
errCh := make(chan error, 1)
go func() {
// Simulate API call
time.Sleep(200 * time.Millisecond)
resultCh <- fmt.Sprintf("data from %s", url)
}()
select {
case result := <-resultCh:
return result, nil
case err := <-errCh:
return "", err
case <-ctx.Done():
// Context cancelled or timed out
return "", ctx.Err()
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancel()
result, err := fetchFromAPI(ctx, "https://api.example.com/data")
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Println(result)
}I selezionatori verificano due aspetti tramite select: la comprensione della regola di selezione casuale e la capacità di combinare i channel con context.Context per implementare pattern di timeout e cancellazione.
Pattern di Concorrenza Frequenti nei Colloqui
I colloqui Go di livello senior includono quasi sempre la richiesta di implementare uno di questi go concurrency patterns da zero.
Il Pattern Fan-Out/Fan-In
D: Implementare una pipeline fan-out/fan-in che elabora elementi in modo concorrente.
Il fan-out distribuisce il lavoro su più goroutine. Il fan-in raccoglie i risultati da più goroutine in un singolo channel.
package main
import (
"fmt"
"sync"
)
// generator produces values on a channel
func generator(nums ...int) <-chan int {
out := make(chan int)
go func() {
for _, n := range nums {
out <- n
}
close(out)
}()
return out
}
// square reads from input, squares each value
func square(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for n := range in {
out <- n * n
}
close(out)
}()
return out
}
// fanIn merges multiple channels into one
func fanIn(channels ...<-chan int) <-chan int {
var wg sync.WaitGroup
merged := make(chan int)
for _, ch := range channels {
wg.Add(1)
go func(c <-chan int) {
defer wg.Done()
for val := range c {
merged <- val
}
}(ch)
}
go func() {
wg.Wait()
close(merged) // close after all inputs are drained
}()
return merged
}
func main() {
in := generator(2, 3, 4, 5, 6)
// Fan out: two goroutines reading from same channel
c1 := square(in)
c2 := square(in)
// Fan in: merge results
for result := range fanIn(c1, c2) {
fmt.Println(result)
}
}L'aspetto fondamentale che i selezionatori cercano: il channel del generator è condiviso tra c1 e c2, quindi ogni valore viene elaborato da esattamente un worker (senza duplicazione). La funzione fanIn utilizza un WaitGroup per sapere quando tutti i channel di input sono stati svuotati prima di chiudere il channel unificato.
Worker Pool con errgroup
D: Come si implementa un worker pool a concorrenza limitata con gestione degli errori?
Il pacchetto golang.org/x/sync/errgroup (parte della libreria standard estesa di Go) risolve questo problema in modo pulito. Gestisce il ciclo di vita delle goroutine, raccoglie il primo errore e si integra con context per la cancellazione.
package main
import (
"context"
"fmt"
"golang.org/x/sync/errgroup"
)
func processItem(ctx context.Context, id int) error {
// Check for cancellation before heavy work
select {
case <-ctx.Done():
return ctx.Err()
default:
}
if id == 7 {
return fmt.Errorf("failed to process item %d", id)
}
fmt.Printf("processed item %d\n", id)
return nil
}
func main() {
g, ctx := errgroup.WithContext(context.Background())
g.SetLimit(3) // maximum 3 concurrent goroutines
for i := 0; i < 10; i++ {
id := i
g.Go(func() error {
return processItem(ctx, id)
})
}
// Wait blocks until all goroutines finish
// Returns the first non-nil error
if err := g.Wait(); err != nil {
fmt.Println("pipeline error:", err)
}
}Con Go 1.24 (la release stabile corrente a inizio 2026), questo pattern resta l'approccio raccomandato. Il metodo SetLimit, introdotto in Go 1.20, elimina la necessità di implementare manualmente un semaforo per limitare la concorrenza.
Race Condition e Primitive sync
D: Come si rilevano e prevengono le race condition in Go?
Go fornisce un race detector integrato, attivabile con il flag -race. Questo strumento rileva a runtime gli accessi concorrenti non sincronizzati alla memoria condivisa.
package main
import (
"fmt"
"sync"
"sync/atomic"
)
// BAD: race condition — do not use in production
func unsafeCounter() int {
counter := 0
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter++ // DATA RACE: concurrent read/write
}()
}
wg.Wait()
return counter // result is non-deterministic
}
// GOOD: atomic operations for simple counters
func atomicCounter() int64 {
var counter atomic.Int64
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter.Add(1) // thread-safe atomic increment
}()
}
wg.Wait()
return counter.Load() // always 1000
}
// GOOD: mutex for complex shared state
type SafeMap struct {
mu sync.RWMutex
data map[string]int
}
func (m *SafeMap) Set(key string, val int) {
m.mu.Lock() // exclusive lock for writes
defer m.mu.Unlock()
m.data[key] = val
}
func (m *SafeMap) Get(key string) (int, bool) {
m.mu.RLock() // shared lock for reads
defer m.mu.RUnlock()
v, ok := m.data[key]
return v, ok
}
func main() {
fmt.Println("unsafe:", unsafeCounter()) // unpredictable
fmt.Println("atomic:", atomicCounter()) // always 1000
}La risposta da colloquio dovrebbe coprire tre strategie di sincronizzazione: sync.Mutex / sync.RWMutex per lo stato condiviso complesso, sync/atomic per contatori e flag semplici, e i channel per la comunicazione tra goroutine ("share memory by communicating, don't communicate by sharing memory"). L'esecuzione di go test -race ./... dovrebbe far parte di ogni pipeline CI.
Context e Pattern di Cancellazione
D: Spiegare come context.Context controlla il ciclo di vita delle goroutine.
Il pacchetto context fornisce un meccanismo per propagare segnali di cancellazione, scadenze e valori legati alla richiesta attraverso i confini delle goroutine. Ogni goroutine a lunga esecuzione dovrebbe accettare un context.Context come primo parametro.
package main
import (
"context"
"fmt"
"time"
)
// worker simulates a long-running task
func worker(ctx context.Context, id int, results chan<- string) {
select {
case <-time.After(time.Duration(id*100) * time.Millisecond):
results <- fmt.Sprintf("worker %d: done", id)
case <-ctx.Done():
results <- fmt.Sprintf("worker %d: cancelled (%v)", id, ctx.Err())
}
}
func main() {
// Parent context with 250ms deadline
ctx, cancel := context.WithTimeout(context.Background(), 250*time.Millisecond)
defer cancel()
results := make(chan string, 5)
// Launch 5 workers with increasing durations
for i := 1; i <= 5; i++ {
go worker(ctx, i, results)
}
// Collect all results
for i := 0; i < 5; i++ {
fmt.Println(<-results)
}
}I worker 1 e 2 completano l'esecuzione entro la scadenza di 250ms. I worker 3, 4 e 5 ricevono il segnale di cancellazione tramite ctx.Done(). Questo pattern è fondamentale per la costruzione di server HTTP e microservizi resilienti in Go: ogni handler di richiesta riceve un context che propaga la cancellazione quando il client si disconnette.
Non si deve mai memorizzare un context.Context in un campo di una struct. La documentazione ufficiale di Go afferma esplicitamente: "Do not store Contexts inside a struct type; instead, pass a Context explicitly to each function that needs it." I selezionatori usano questa domanda per verificare se il candidato segue le convenzioni idiomatiche del linguaggio.
Rilevamento dei Deadlock: Domande Insidiose
D: Questo codice provoca un deadlock? Perché?
Le domande sui deadlock sono molto diffuse perché testano la capacità del candidato di ragionare sullo scheduling delle goroutine e sulle operazioni dei channel.
package main
func main() {
ch := make(chan int)
ch <- 42 // DEADLOCK: unbuffered send with no receiver
// The main goroutine blocks here forever
// Go runtime detects this: "fatal error: all goroutines are asleep"
}La correzione è immediata: rendere il channel buffered (make(chan int, 1)) oppure lanciare una goroutine che riceve prima dell'invio. Il runtime Go rileva i deadlock quando tutte le goroutine sono bloccate, ma solo quando tutte le goroutine sono in stato di attesa. Se anche una sola goroutine è in esecuzione (ad esempio un server HTTP in background), il runtime non rileva un deadlock parziale.
Il runtime Go rileva i deadlock solo quando ogni goroutine nel programma è bloccata. Nelle applicazioni reali con server HTTP o worker in background, le goroutine in leak che si trovano in deadlock non attivano il rilevatore del runtime. Strumenti come pprof e i dump delle goroutine (runtime.Stack) sono necessari per diagnosticare questi problemi in produzione.
Elaborazione Concorrente con Rate Limiting
D: Come si implementano chiamate API concorrenti con rate limiting?
Questa domanda verifica la capacità di combinare più primitive di concorrenza in una soluzione coerente. Il pattern richiede sia il controllo del numero massimo di operazioni simultanee sia la regolazione della frequenza temporale delle richieste.
package main
import (
"context"
"fmt"
"sync"
"time"
)
// RateLimiter controls concurrent and temporal access
type RateLimiter struct {
semaphore chan struct{} // limits concurrency
ticker *time.Ticker // limits rate
}
func NewRateLimiter(maxConcurrent int, interval time.Duration) *RateLimiter {
return &RateLimiter{
semaphore: make(chan struct{}, maxConcurrent),
ticker: time.NewTicker(interval),
}
}
func (rl *RateLimiter) Execute(ctx context.Context, fn func() error) error {
// Wait for rate limit tick
select {
case <-rl.ticker.C:
case <-ctx.Done():
return ctx.Err()
}
// Acquire concurrency slot
select {
case rl.semaphore <- struct{}{}:
case <-ctx.Done():
return ctx.Err()
}
defer func() { <-rl.semaphore }() // release slot
return fn()
}
func main() {
rl := NewRateLimiter(3, 100*time.Millisecond)
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
err := rl.Execute(ctx, func() error {
fmt.Printf("[%v] processing %d\n", time.Now().Format("04:05.000"), id)
time.Sleep(150 * time.Millisecond) // simulate work
return nil
})
if err != nil {
fmt.Printf("item %d: %v\n", id, err)
}
}(i)
}
wg.Wait()
}Questo pattern combina un semaforo basato su channel (per limitare la concorrenza) con un ticker (per limitare la frequenza). Il doppio select con verifica del context garantisce uno shutdown ordinato. Si tratta del tipo di risposta production-ready che distingue i candidati senior durante un colloquio tecnico su golang goroutines e go channels.
Inizia a praticare!
Metti alla prova le tue conoscenze con i nostri simulatori di colloquio e test tecnici.
Conclusione
- Le goroutine sono thread in user space gestiti dal runtime Go con scheduling M:N; si consiglia di recuperare sempre i panic nelle goroutine lanciate
- I channel unbuffered sincronizzano mittente e destinatario; i channel buffered disaccoppiano la temporizzazione — la scelta dipende dalla necessità di conferma da parte del mittente
- L'istruzione
selectmultiplexa le operazioni sui channel con selezione casuale quando più casi sono pronti; va combinata concontext.Contextper timeout e cancellazione - Fan-out/fan-in e worker pool (tramite
errgroup.SetLimit) sono i due pattern di concorrenza più richiesti nei colloqui - Per lo stato condiviso complesso si utilizza
sync.Mutex, per contatori semplicisync/atomic, e i channel per la comunicazione tra goroutine - L'esecuzione di
go test -racenella pipeline CI è indispensabile per intercettare le data race; i deadlock parziali richiedonopprofper la diagnosi - Il
context.Contextnon va mai memorizzato in una struct: si passa come primo parametro di funzione - Il rate limiting in Go combina semafori a channel con ticker, incapsulati in istruzioni select consapevoli del context
Inizia a praticare!
Metti alla prova le tue conoscenze con i nostri simulatori di colloquio e test tecnici.
Tag
Condividi
Articoli correlati

Concorrenza in Go: Goroutine e Canali - Guida Completa
Padroneggia la concorrenza in Go con goroutine e canali. Pattern avanzati, sincronizzazione, istruzioni select e best practice con esempi di codice dettagliati.

Top 25 domande di colloquio Go: guida completa per sviluppatori
Padroneggia i colloqui Go con le 25 domande più frequenti. Goroutine, channel, interfacce e pattern di concorrenza con esempi di codice.

Go: Fondamenti per Sviluppatori Java/Python nel 2026
Imparare Go rapidamente sfruttando l'esperienza in Java o Python. Goroutines, channels, interfaces e pattern essenziali per una transizione fluida.