I design pattern non sono una checklist. In Go, applicare pattern in stile Java porta rapidamente a codice che appare estraneo a ogni sviluppatore Go che lo legge. I pattern qui sotto sono presentati nella loro forma idiomatica Go, spesso molto piu semplice della versione da libro di testo.
Singleton con sync.Once#
Il singleton canonico in Go usa sync.Once, che garantisce l’esecuzione dell’inizializzatore esattamente una volta, indipendentemente da quante goroutine chiamano GetInstance contemporaneamente. Non serve il doppio lock, nessuna keyword volatile: il runtime gestisce tutto internamente.
package config
import "sync"
type AppConfig struct {
DatabaseURL string
Port int
Debug bool
}
var (
instance *AppConfig
once sync.Once
)
func GetConfig() *AppConfig {
once.Do(func() {
instance = &AppConfig{
DatabaseURL: "postgres://localhost/mydb",
Port: 8080,
Debug: false,
}
})
return instance
}sync.Once e sicuro per l’uso concorrente. Una volta che Do e stato eseguito, le chiamate successive ritornano immediatamente senza acquisire alcun lock. Questo lo rende essenzialmente gratuito sul percorso caldo dopo l’inizializzazione.
Il pattern e appropriato per stato veramente globale: configurazione caricata una volta all’avvio, un pool di connessioni database condiviso, o un registro di metriche. Evitarlo per qualsiasi cosa che deve essere reimpostata tra i test.
Functional Options: il Builder Idiomatico Go#
Il pattern Builder classico esiste in Go, ma l’approccio idiomatico e il pattern delle functional options, introdotto da Rob Pike. Invece di un oggetto builder, si passano funzioni di opzione variadiche che modificano una struct di configurazione prima che l’oggetto reale venga costruito.
package server
import (
"net/http"
"time"
)
type Server struct {
addr string
readTimeout time.Duration
writeTimeout time.Duration
maxHeaderBytes int
handler http.Handler
}
// ServerOption e una funzione che configura un Server.
type ServerOption func(*Server)
func WithAddr(addr string) ServerOption {
return func(s *Server) {
s.addr = addr
}
}
func WithReadTimeout(d time.Duration) ServerOption {
return func(s *Server) {
s.readTimeout = d
}
}
func WithWriteTimeout(d time.Duration) ServerOption {
return func(s *Server) {
s.writeTimeout = d
}
}
func WithMaxHeaderBytes(n int) ServerOption {
return func(s *Server) {
s.maxHeaderBytes = n
}
}
func NewServer(handler http.Handler, opts ...ServerOption) *Server {
s := &Server{
addr: ":8080",
readTimeout: 5 * time.Second,
writeTimeout: 10 * time.Second,
maxHeaderBytes: 1 << 20,
handler: handler,
}
for _, opt := range opts {
opt(s)
}
return s
}
func (s *Server) ListenAndServe() error {
srv := &http.Server{
Addr: s.addr,
Handler: s.handler,
ReadTimeout: s.readTimeout,
WriteTimeout: s.writeTimeout,
MaxHeaderBytes: s.maxHeaderBytes,
}
return srv.ListenAndServe()
}Punto di chiamata:
srv := server.NewServer(
myHandler,
server.WithAddr(":9090"),
server.WithReadTimeout(30 * time.Second),
)Questo approccio e superiore al Builder classico perche i valori di default risiedono in un unico posto (NewServer), le funzioni di opzione sono testabili e componibili singolarmente, e i chiamanti specificano solo cio che vogliono sovrascrivere.
Factory per la Costruzione Basata su Interfacce#
Si usa una factory quando si ha bisogno di produrre tipi concreti diversi dietro un’interfaccia condivisa. Un esempio realistico: un sender di notifiche con backend Email, SMS e Slack.
package notify
import "fmt"
// Sender e l'interfaccia che ogni backend di notifica deve implementare.
type Sender interface {
Send(to, subject, body string) error
}
type EmailSender struct{ SMTPHost string }
func (e *EmailSender) Send(to, subject, body string) error {
fmt.Printf("[EMAIL] to=%s subject=%s\n", to, subject)
return nil
}
type SMSSender struct{ APIKey string }
func (s *SMSSender) Send(to, subject, body string) error {
fmt.Printf("[SMS] to=%s body=%s\n", to, body)
return nil
}
type SlackSender struct{ WebhookURL string }
func (sl *SlackSender) Send(to, subject, body string) error {
fmt.Printf("[SLACK] channel=%s body=%s\n", to, body)
return nil
}
// NewSender e la factory function.
func NewSender(provider string, config map[string]string) (Sender, error) {
switch provider {
case "email":
return &EmailSender{SMTPHost: config["smtp_host"]}, nil
case "sms":
return &SMSSender{APIKey: config["api_key"]}, nil
case "slack":
return &SlackSender{WebhookURL: config["webhook_url"]}, nil
default:
return nil, fmt.Errorf("provider di notifica sconosciuto: %s", provider)
}
}Restituire (Sender, error) dalla factory, mai solo Sender. I problemi di configurazione devono emergere al momento della costruzione, non quando il primo messaggio viene inviato alle 3 di notte.
Observer con Channels: Implementazione Concorrente Corretta#
L’implementazione naive dell’Observer con channel non bufferizzati causa deadlock o panic. Usare un WaitGroup e channel bufferizzati risolve entrambi i problemi: il mittente non si blocca mai, e la cleanup attende che tutte le goroutine finiscano di elaborare.
package events
import (
"fmt"
"sync"
)
type Observable struct {
observers []chan int
}
func (o *Observable) AddObserver(c chan int) {
o.observers = append(o.observers, c)
}
// Notify invia n a tutti gli observer senza bloccarsi.
// Ogni channel observer deve essere bufferizzato almeno quanto la dimensione massima del burst atteso.
func (o *Observable) Notify(n int) {
for _, c := range o.observers {
c <- n
}
}
// CloseAll segnala a tutte le goroutine observer di fermarsi chiudendo i loro channel.
func (o *Observable) CloseAll() {
for _, c := range o.observers {
close(c)
}
}
func Example() {
observable := &Observable{}
// I channel bufferizzati impediscono a Notify di bloccarsi.
observer1 := make(chan int, 10)
observer2 := make(chan int, 10)
observable.AddObserver(observer1)
observable.AddObserver(observer2)
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
for n := range observer1 {
fmt.Println("Observer 1 ricevuto:", n)
}
}()
go func() {
defer wg.Done()
for n := range observer2 {
fmt.Println("Observer 2 ricevuto:", n)
}
}()
observable.Notify(1)
observable.Notify(2)
observable.Notify(3)
// Chiudi i channel per segnalare alle goroutine di uscire, poi aspetta.
observable.CloseAll()
wg.Wait()
}Il WaitGroup garantisce che tutti gli eventi vengano elaborati prima che il programma termini.
Strategy Pattern con Funzioni di Prima Classe#
In Java, Strategy richiede un’interfaccia con un singolo metodo piu una famiglia di classi implementanti. In Go, un tipo funzione e gia una strategy. Nessuna interfaccia, nessuna struct, nessun insieme di metodi.
package sorter
// SortStrategy e un tipo funzione. Qualsiasi funzione con questa firma e una strategy.
type SortStrategy func(data []int)
type Sorter struct {
strategy SortStrategy
}
func NewSorter(s SortStrategy) *Sorter {
return &Sorter{strategy: s}
}
func (s *Sorter) Sort(data []int) {
s.strategy(data)
}
func BubbleSort(data []int) {
n := len(data)
for i := 0; i < n-1; i++ {
for j := 0; j < n-1-i; j++ {
if data[j] > data[j+1] {
data[j], data[j+1] = data[j+1], data[j]
}
}
}
}
func QuickSort(data []int) {
if len(data) <= 1 {
return
}
pivot := data[len(data)/2]
var left, right, equal []int
for _, v := range data {
switch {
case v < pivot:
left = append(left, v)
case v > pivot:
right = append(right, v)
default:
equal = append(equal, v)
}
}
QuickSort(left)
QuickSort(right)
copy(data, append(append(left, equal...), right...))
}Decorator Pattern con Middleware HTTP#
In Go, il middleware HTTP e l’implementazione canonica del Decorator. Un http.Handler viene avvolto da una funzione che aggiunge funzionalita trasversali (logging, autenticazione, rate limiting) senza modificare il handler interno.
package middleware
import (
"log"
"net/http"
"time"
)
// Logger avvolge un http.Handler per registrare la durata delle richieste.
func Logger(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
log.Printf("%s %s %s", r.Method, r.URL.Path, time.Since(start))
})
}
// RequireAuth avvolge un http.Handler per imporre l'autenticazione bearer token.
func RequireAuth(token string, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Authorization") != "Bearer "+token {
http.Error(w, "non autorizzato", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}Concatenare i decorator al punto di chiamata:
var handler http.Handler = myAPIHandler
handler = middleware.Logger(handler)
handler = middleware.RequireAuth(os.Getenv("API_TOKEN"), handler)
http.ListenAndServe(":8080", handler)Quando NON Usare Questi Pattern#
Il modello di composizione di Go spesso rende i pattern non necessari o addirittura controproducenti.
| Situazione | Pattern che potresti usare | Cosa fare in Go invece |
|---|---|---|
| Stato globale condiviso | Singleton | Passa le dipendenze come argomenti; usa sync.Once solo quando l’init globale e davvero necessaria |
| Configurare una struct | Builder | Functional options o una plain struct con zero value sensati |
| Dispatch polimorfico | Interfaccia Strategy | Tipo funzione o interfaccia con un unico metodo |
| Aggiungere comportamento a un tipo | Decorator | Incorpora il tipo in una nuova struct e aggiungi/sovrascrivi metodi |
Simulare l’ereditarieta in stile Java in Go (incorporare struct per “estenderle”) produce codice difficile da seguire e rompe la convenzione Go che la composizione sia esplicita. Se ti trovi a chiederti “come posso fare in modo che una struct erediti da un’altra struct”, fermati e riconsideri il modello dei dati.
Se vuoi approfondire questi temi, offro sessioni di coaching 1:1 per ingegneri che lavorano su integrazione AI, architettura cloud e platform engineering. Prenota una sessione (50 EUR / 60 min) o scrivimi a manuel.fedele+website@gmail.com.