# 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. - Published: 2026-05-28 - Updated: 2026-05-28 - Author: SharpSkill - Tags: go, design-patterns, interview, best-practices - Reading time: 12 min --- 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. > **Principio idiomatico di 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. ```go // server.go 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. ```go // payment.go 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. ```go // connpool.go 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. ## 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. ```go // eventbus.go 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`. ```go // middleware.go 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. ```go // models.go 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. > **Suggerimento per il colloquio** > > 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()`. > **Attenzione** > > 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. ## 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 `NewXxx` con 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. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/it/blog/go/go-design-patterns-essential-patterns-interview