Go Design Patterns: Pattern essenziali e domande da colloquio per sviluppatori Go
I sei design pattern Go piu importanti con codice pronto per la produzione: Functional Options, Strategy, Factory, Observer, Middleware e Struct Embedding. Con domande frequenti nei colloqui tecnici.

I programmi Go, grazie alla semplicita del linguaggio e alla sua filosofia orientata alla composizione, richiedono un approccio ai design pattern diverso rispetto ai linguaggi orientati agli oggetti tradizionali. Go non dispone di classi, ereditarieta o generics nel senso classico: i pattern si costruiscono attorno a interfacce, funzioni di primo ordine, goroutine e canali. Comprendere questi pattern idiomatici risulta fondamentale sia per scrivere codice di produzione robusto, sia per affrontare con sicurezza i colloqui tecnici. Questa guida analizza i design pattern piu rilevanti in Go, con esempi pratici e domande frequenti nei colloqui per sviluppatori Go.
Go adotta un approccio "composition over inheritance" come principio fondamentale del linguaggio. A differenza di Java o C++, i pattern in Go si basano su interfacce implicite, embedding di struct e funzioni come valori di prima classe. Pensare in termini di composizione, anziche di gerarchie, rappresenta la chiave per scrivere codice Go idiomatico.
Functional Options: configurazione flessibile e leggibile
Il pattern Functional Options risolve un problema comune nella progettazione di API Go: come configurare una struct complessa senza ricorrere a costruttori con decine di parametri o a struct di configurazione separate. Questo approccio, reso popolare da Dave Cheney e Rob Pike, sfrutta le closure per applicare opzioni in modo incrementale a una struct con valori di default sensati.
Il vantaggio principale risiede nella retrocompatibilita: aggiungere nuove opzioni non rompe il codice esistente, poiche i chiamanti specificano solo cio che desiderano modificare. Nei colloqui tecnici sui golang design patterns, questo pattern viene citato frequentemente come esempio di API design idiomatico.
package server
import (
"time"
"log/slog"
)
// Server holds the HTTP server configuration.
type Server struct {
host string
port int
timeout time.Duration
maxConns int
logger *slog.Logger
}
// Option defines a functional option for Server.
type Option func(*Server)
// WithPort sets the server port.
func WithPort(port int) Option {
return func(s *Server) {
s.port = port
}
}
// WithTimeout sets the request timeout.
func WithTimeout(d time.Duration) Option {
return func(s *Server) {
s.timeout = d
}
}
// WithMaxConns sets the maximum concurrent connections.
func WithMaxConns(n int) Option {
return func(s *Server) {
s.maxConns = n
}
}
// New creates a Server with sensible defaults and applies options.
func New(host string, opts ...Option) *Server {
srv := &Server{
host: host,
port: 8080, // default port
timeout: 30 * time.Second, // default timeout
maxConns: 100, // default max connections
logger: slog.Default(),
}
for _, opt := range opts {
opt(srv)
}
return srv
}La firma New(host string, opts ...Option) mantiene obbligatorio solo il parametro essenziale (host), delegando tutto il resto a opzioni variadic. Ogni Option e una closure che modifica un singolo campo, rendendo il codice auto-documentante: server.New("localhost", server.WithPort(9090), server.WithTimeout(10*time.Second)).
Strategy Pattern: algoritmi intercambiabili tramite interfacce
Lo Strategy Pattern permette di definire una famiglia di algoritmi, incapsularli e renderli intercambiabili a runtime. In Go, questo pattern si implementa in modo naturale attraverso le interfacce: si definisce un contratto (l'interfaccia) e si forniscono implementazioni concrete che lo soddisfano.
L'aspetto distintivo di Go risiede nelle interfacce implicite: un tipo soddisfa un'interfaccia semplicemente implementandone i metodi, senza dichiarazioni esplicite. Questo riduce l'accoppiamento e facilita il testing, poiche creare mock diventa banale.
package payment
import "fmt"
// Processor defines the strategy interface.
type Processor interface {
Pay(amount float64) (string, error)
}
// CreditCard implements Processor for card payments.
type CreditCard struct {
CardNumber string
Expiry string
}
func (c *CreditCard) Pay(amount float64) (string, error) {
// Charge the card via payment gateway
return fmt.Sprintf("charged %.2f to card ending %s", amount, c.CardNumber[len(c.CardNumber)-4:]), nil
}
// BankTransfer implements Processor for wire transfers.
type BankTransfer struct {
IBAN string
}
func (b *BankTransfer) Pay(amount float64) (string, error) {
return fmt.Sprintf("initiated transfer of %.2f to %s", amount, b.IBAN), nil
}
// Checkout processes a payment using the given strategy.
func Checkout(p Processor, amount float64) error {
receipt, err := p.Pay(amount)
if err != nil {
return fmt.Errorf("payment failed: %w", err)
}
fmt.Println(receipt)
return nil
}La funzione Checkout accetta qualsiasi valore che soddisfi Processor, senza conoscere i dettagli implementativi. Aggiungere un nuovo metodo di pagamento (ad esempio criptovalute) richiede solo una nuova struct con il metodo Pay, senza modificare alcun codice esistente. Questo rispetta il principio Open/Closed e rappresenta una delle go patterns best practices piu consolidate.
Factory Functions: costruzione sicura con validazione
A differenza dei linguaggi con costruttori integrati, Go utilizza funzioni factory convenzionalmente denominate New o NewXxx. Queste funzioni restituiscono un puntatore alla struct e un errore, consentendo la validazione dei parametri al momento della creazione. Questo pattern garantisce che ogni istanza si trovi in uno stato valido fin dalla nascita.
package pool
import (
"errors"
"sync"
)
// ConnPool manages a pool of reusable connections.
type ConnPool struct {
mu sync.Mutex
conns []Conn
maxSize int
}
// Conn represents a database connection.
type Conn struct {
ID int
Active bool
}
// NewConnPool validates parameters and returns an initialized pool.
func NewConnPool(maxSize int) (*ConnPool, error) {
if maxSize <= 0 {
return nil, errors.New("pool: maxSize must be positive")
}
return &ConnPool{
conns: make([]Conn, 0, maxSize),
maxSize: maxSize,
}, nil
}
// Acquire returns a connection from the pool.
func (p *ConnPool) Acquire() (*Conn, error) {
p.mu.Lock()
defer p.mu.Unlock()
for i := range p.conns {
if !p.conns[i].Active {
p.conns[i].Active = true
return &p.conns[i], nil
}
}
if len(p.conns) >= p.maxSize {
return nil, errors.New("pool: no available connections")
}
c := Conn{ID: len(p.conns) + 1, Active: true}
p.conns = append(p.conns, c)
return &p.conns[len(p.conns)-1], nil
}Il pattern (*T, error) come tipo di ritorno costituisce una convenzione idiomatica di Go che forza il chiamante a gestire esplicitamente l'errore. Nel caso del connection pool, NewConnPool rifiuta valori non validi (maxSize <= 0) prima ancora di allocare risorse. L'uso di sync.Mutex protegge l'accesso concorrente, un requisito critico nel codice di produzione Go.
Pronto a superare i tuoi colloqui su Go?
Pratica con i nostri simulatori interattivi, flashcards e test tecnici.
Observer Pattern con i canali Go
Il pattern Observer, tradizionalmente implementato con callback e liste di listener, trova in Go un'espressione naturale attraverso i canali (channels). I canali forniscono comunicazione type-safe tra goroutine, rendendo la pubblicazione e la sottoscrizione di eventi un'operazione concorrente e sicura senza bisogno di lock espliciti lato consumatore.
package events
import "sync"
// Event carries a topic and payload.
type Event struct {
Topic string
Payload any
}
// Bus manages subscriptions and event dispatch.
type Bus struct {
mu sync.RWMutex
subscribers map[string][]chan Event
}
// NewBus creates an event bus.
func NewBus() *Bus {
return &Bus{
subscribers: make(map[string][]chan Event),
}
}
// Subscribe returns a channel that receives events for a topic.
func (b *Bus) Subscribe(topic string) <-chan Event {
ch := make(chan Event, 16) // buffered to avoid blocking publisher
b.mu.Lock()
b.subscribers[topic] = append(b.subscribers[topic], ch)
b.mu.Unlock()
return ch
}
// Publish sends an event to all subscribers of the topic.
func (b *Bus) Publish(topic string, payload any) {
b.mu.RLock()
defer b.mu.RUnlock()
for _, ch := range b.subscribers[topic] {
select {
case ch <- Event{Topic: topic, Payload: payload}:
default:
// subscriber too slow, drop event
}
}
}L'implementazione utilizza canali bufferizzati (capacita 16) per evitare che un publisher lento blocchi l'intero bus. Il costrutto select con caso default in Publish implementa una politica di drop: se un sottoscrittore non consuma abbastanza velocemente, l'evento viene scartato anziche bloccare il publisher. In un sistema di produzione, sarebbe opportuno aggiungere metriche per monitorare gli eventi persi e un meccanismo di Unsubscribe che chiuda il canale.
Middleware Pattern: composizione di handler HTTP
Il pattern Middleware rappresenta una delle go design patterns piu utilizzate nello sviluppo web con Go. La libreria standard net/http definisce l'interfaccia Handler con un singolo metodo, rendendo la composizione di middleware estremamente naturale: ogni middleware accetta un http.Handler, lo avvolge con logica aggiuntiva e restituisce un nuovo http.Handler.
package middleware
import (
"log/slog"
"net/http"
"time"
)
// Logging records request duration and status.
func Logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
wrapped := &statusWriter{ResponseWriter: w, status: 200}
next.ServeHTTP(wrapped, r)
slog.Info("request",
"method", r.Method,
"path", r.URL.Path,
"status", wrapped.status,
"duration", time.Since(start),
)
})
}
// statusWriter captures the HTTP status code.
type statusWriter struct {
http.ResponseWriter
status int
}
func (w *statusWriter) WriteHeader(code int) {
w.status = code
w.ResponseWriter.WriteHeader(code)
}
// Chain applies middleware in order: first listed = outermost.
func Chain(h http.Handler, mw ...func(http.Handler) http.Handler) http.Handler {
for i := len(mw) - 1; i >= 0; i-- {
h = mw[i](h)
}
return h
}La tecnica del statusWriter merita attenzione: poiche http.ResponseWriter non espone lo status code dopo la scrittura, si utilizza un wrapper che intercetta la chiamata WriteHeader. La funzione Chain applica i middleware in ordine inverso affinche il primo della lista sia il piu esterno nella catena di esecuzione. Questo pattern si compone elegantemente: Chain(handler, Logging, Auth, RateLimit) crea una pipeline chiara e manutenibile.
Composizione tramite Struct Embedding
Go non supporta l'ereditarieta, ma offre l'embedding di struct come meccanismo di composizione. Quando una struct viene embedded in un'altra, tutti i suoi campi e metodi vengono "promossi" e diventano accessibili direttamente sulla struct contenitore. Questo approccio favorisce il riutilizzo del codice senza creare gerarchie di tipi fragili.
package models
import "time"
// Timestamps provides common audit fields.
type Timestamps struct {
CreatedAt time.Time
UpdatedAt time.Time
}
// User embeds Timestamps to gain CreatedAt/UpdatedAt.
type User struct {
Timestamps
ID int
Email string
}
// Order also embeds Timestamps.
type Order struct {
Timestamps
ID int
UserID int
Total float64
}Con l'embedding, User e Order dispongono automaticamente dei campi CreatedAt e UpdatedAt. Se Timestamps avesse metodi (ad esempio MarkUpdated()), anche questi sarebbero disponibili direttamente su User e Order. A differenza dell'ereditarieta, l'embedding non crea una relazione "is-a" ma "has-a": un User non e un Timestamps, ma contiene un Timestamps. Questa distinzione risulta fondamentale nei colloqui tecnici su golang design patterns interview.
Domande frequenti nei colloqui tecnici su Go Design Patterns
Questa sezione raccoglie le domande piu comuni poste durante i colloqui tecnici per posizioni Go, con risposte concise e orientate alla pratica.
Perche Go preferisce la composizione all'ereditarieta?
Go e stato progettato intenzionalmente senza ereditarieta di classe. La composizione tramite embedding e interfacce implicite produce codice piu flessibile e manutenibile. L'ereditarieta crea accoppiamento stretto tra classi base e derivate (il "fragile base class problem"), mentre la composizione permette di assemblare comportamenti in modo indipendente. Le interfacce implicite di Go eliminano la necessita di dichiarare le dipendenze in anticipo, facilitando il refactoring e il testing.
Nei colloqui tecnici, spiegare la differenza tra embedding (composizione, "has-a") e ereditarieta ("is-a") dimostra una comprensione profonda della filosofia Go. Accompagnare la spiegazione con un esempio concreto, come il pattern Timestamps mostrato sopra, rende la risposta immediatamente credibile.
Come si implementa il Singleton in Go in modo concurrency-safe?
Il package sync fornisce sync.Once, che garantisce l'esecuzione di una funzione una sola volta, indipendentemente dal numero di goroutine che la invocano. L'approccio idiomatico prevede una variabile package-level e una funzione GetInstance() che utilizza once.Do(). Tuttavia, nella community Go il Singleton viene spesso considerato un anti-pattern: rende il testing difficile e introduce stato globale. L'alternativa consigliata consiste nell'iniettare le dipendenze esplicitamente attraverso i costruttori.
Quando utilizzare il pattern Functional Options rispetto a una struct di configurazione?
Il pattern Functional Options risulta preferibile quando l'API deve restare stabile nel tempo e i parametri opzionali sono numerosi. Ogni opzione e auto-documentante e i valori di default sono centralizzati nel costruttore. Una struct di configurazione (Config struct) funziona meglio quando i parametri sono pochi, strettamente correlati e vengono spesso serializzati/deserializzati (ad esempio da file YAML). In pratica, molte librerie Go di successo (gRPC, Zap logger) utilizzano Functional Options per le API pubbliche.
Come si gestisce la propagazione degli errori nei pattern Go?
Go utilizza valori di errore espliciti anziche eccezioni. Nei pattern come Factory e Strategy, ogni operazione restituisce (risultato, error) e il chiamante decide come reagire. Il wrapping degli errori con fmt.Errorf("contesto: %w", err) permette di costruire una catena di contesto senza perdere l'errore originale. I tipi di errore personalizzati (sentinel errors o error types) consentono ai chiamanti di distinguere tra diverse condizioni di errore tramite errors.Is() e errors.As().
Durante un colloquio su go patterns best practices, mostrare familiarita con errors.Is(), errors.As() e il wrapping con %w dimostra padronanza della gestione errori idiomatica in Go. Evitare di menzionare panic/recover come meccanismo principale di gestione errori: in Go, panic e riservato a situazioni irrecuperabili.
Pronto a superare i tuoi colloqui su Go?
Pratica con i nostri simulatori interattivi, flashcards e test tecnici.
Conclusione
I design pattern in Go riflettono la filosofia pragmatica del linguaggio: semplicita, composizione e concorrenza come primitive di prima classe. Ecco un riepilogo dei pattern trattati e del loro ambito di applicazione:
- Functional Options: configurazione flessibile e retrocompatibile per struct complesse, ideale per librerie pubbliche
- Strategy Pattern: algoritmi intercambiabili tramite interfacce implicite, perfetto per logiche di business variabili (pagamenti, notifiche, storage)
- Factory Functions: costruzione sicura con validazione integrata, convenzione idiomatica
NewXxxcon restituzione(*T, error) - Observer con Channels: pubblicazione/sottoscrizione di eventi sfruttando le primitive di concorrenza native di Go
- Middleware Pattern: composizione di handler HTTP tramite funzioni wrapper, fondamento dello sviluppo web in Go
- Struct Embedding: riutilizzo del codice tramite composizione anziche ereditarieta, promuovendo relazioni "has-a" pulite
Padroneggiare questi golang interview patterns non significa solo superare un colloquio tecnico: significa scrivere codice Go che risulta naturale, manutenibile e performante in ambienti di produzione. La chiave risiede nel comprendere non solo il "come", ma il "perche" dietro ogni pattern, e nel saper riconoscere quando un pattern semplifica genuinamente il codice e quando invece aggiunge complessita non necessaria.
Inizia a praticare!
Metti alla prova le tue conoscenze con i nostri simulatori di colloquio e test tecnici.
Tag
Condividi
Articoli correlati

Go 1.26 Colloquio Tecnico: Green Tea GC, go fix e Ottimizzazioni dello Stack
Domande e risposte tecniche su Go 1.26 per colloqui: Green Tea garbage collector con riduzione overhead del 10-40%, nuovo strumento go fix con modernizers, allocazione slice sullo stack, leak detection delle goroutine e sicurezza post-quantistica.

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.

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.