# Design patterns Go : patterns essentiels et questions d'entretien pour développeurs Go > Maîtrisez les design patterns Go : Functional Options, Strategy, Factory et Observer. Exemples de code concrets, bonnes pratiques idiomatiques et questions d'entretien fréquentes pour développeurs Go. - Published: 2026-05-28 - Updated: 2026-06-06 - Author: SharpSkill - Tags: go, design-patterns, best-practices, interview, software-architecture - Reading time: 10 min --- Les design patterns en Go diffèrent fondamentalement de leurs équivalents dans les langages orientés objet. Sans classes ni héritage, Go s'appuie sur la composition, les interfaces et les fonctions de première classe pour atteindre la même flexibilité structurelle, souvent avec moins de cérémonie et plus de clarté. > **Principe idiomatique en Go** > > Go privilégie la composition à l'héritage. Chaque pattern classique doit être adapté pour fonctionner avec les interfaces, l'embedding de structs et les fonctions comme citoyens de première classe, plutôt qu'avec des hiérarchies de classes. ## Le pattern Functional Options pour une configuration flexible Le pattern Functional Options résout un problème courant en Go : construire des objets avec de nombreux paramètres optionnels. Contrairement aux langages dotés de la surcharge de méthodes ou des arguments par défaut, Go n'offre ni l'un ni l'autre. Le pattern Builder fonctionne mais reste verbeux. Les Functional Options fournissent une API propre et extensible qui demeure rétrocompatible à mesure que les besoins évoluent. Ce pattern, [popularisé par Dave Cheney](https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis) et désormais standard dans l'écosystème Go, utilise des arguments variadiques pour configurer une struct. ```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 } ``` L'appelant ne précise que ce qui diffère des valeurs par défaut. L'ajout d'une nouvelle option ne casse jamais les sites d'appel existants. Ce pattern apparaît dans des bibliothèques de production comme `google.golang.org/grpc` et `go.uber.org/zap`. ## Le pattern Strategy par satisfaction d'interface Le pattern Strategy encapsule des algorithmes interchangeables derrière une interface commune. En Go, cela se traduit directement par du polymorphisme basé sur les interfaces, sans classes abstraites ni chaînes d'héritage. Un système de traitement des paiements illustre ce pattern. Chaque moyen de paiement implémente la même interface, et le service de paiement choisit la stratégie au moment de l'exécution. ```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 } ``` Les interfaces Go sont satisfaites implicitement, sans mot-clé `implements`. Tout type doté d'une méthode `Pay(float64) (string, error)` se qualifie comme `Processor`. Cela maintient un couplage faible et simplifie les tests : il suffit de passer un `Processor` simulé qui renvoie des résultats prévisibles. ## Fonctions factory et patterns de construction en Go Go ne possède pas de constructeurs. Le remplacement idiomatique est une fonction factory, généralement nommée `New` ou `NewXxx`, qui renvoie une struct initialisée. Les fonctions factory imposent des invariants que l'initialisation par valeur zéro ne peut garantir. Cette distinction compte pour la préparation aux entretiens : un développeur Go doit reconnaître quand une fonction factory est nécessaire, par opposition aux cas où la [valeur zéro est utile par défaut](https://go.dev/blog/organizing-go-code). ```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 } ``` La fonction factory `NewConnPool` valide le paramètre `maxSize` et préalloue le slice. L'initialisation directe de la struct (`&ConnPool{}`) sauterait cette validation, ce qui pourrait provoquer des panics à l'exécution. Ce pattern s'étend naturellement : il se combine avec les Functional Options pour des scénarios de configuration plus complexes. ## Le pattern Observer avec les channels et les goroutines Le pattern Observer notifie plusieurs abonnés lorsque l'état change. En Java ou en C#, cela passe par des écouteurs d'événements et l'enregistrement de callbacks. Go propose une voie plus idiomatique : les channels. Chaque abonné reçoit les événements via son propre channel, et des goroutines assurent la livraison de manière concurrente. Cette approche exploite directement les [primitives de concurrence](/blog/go/go-concurrency-goroutines-channels) de Go, en évitant le plat de spaghettis de callbacks. ```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 } } } ``` Le `select` avec une clause `default` empêche un abonné lent de bloquer l'ensemble du bus. Dans les systèmes de production, l'ajout d'un mécanisme d'annulation basé sur le contexte et d'une méthode `Unsubscribe` complèterait l'implémentation. ## Le pattern Middleware pour les pipelines de requêtes HTTP Les chaînes de middleware sont la colonne vertébrale des serveurs HTTP en Go. Le pattern enveloppe un `http.Handler` avec un comportement supplémentaire (journalisation, authentification, limitation de débit) sans modifier le handler lui-même. L'interface `http.Handler` de la bibliothèque standard rend cette composition triviale. ```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 fonction `Chain` applique les middleware dans l'ordre de déclaration. Chaque middleware enveloppe le suivant, formant un pipeline. Ce pattern est identique dans des [routeurs populaires comme chi](https://github.com/go-chi/chi), qui accepte `func(http.Handler) http.Handler` comme middleware. ## La composition plutôt que l'héritage avec l'embedding de structs Go ne prend pas en charge l'héritage. À la place, l'embedding de structs promeut les méthodes d'un type interne vers le type externe, permettant la réutilisation de code sans couplage fort. Ce n'est pas de l'héritage : le type embarqué ignore tout du type qui l'embarque, et il n'y a pas de dispatch virtuel. Comprendre cette distinction est crucial pour les [questions d'entretien Go](/technologies/go/interview-questions/concurrency-patterns). Les candidats qui confondent embedding et héritage révèlent une compréhension superficielle du système de types de Go. ```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 } ``` `User` et `Order` exposent désormais directement les champs `CreatedAt` et `UpdatedAt`. Toutes les méthodes définies sur `Timestamps` sont également promues. La règle clé : l'embedding fournit de la délégation, pas de la substitution. Un `User` n'est pas un `Timestamps` ; il possède un `Timestamps`. ## Questions d'entretien sur les design patterns Go Les questions sur les design patterns en entretien Go testent la capacité d'un candidat à adapter les patterns classiques au système de types de Go. Les questions suivantes reviennent fréquemment lors des entretiens techniques et des sessions sur site. **Q : Quand une fonction factory doit-elle renvoyer une erreur plutôt que de paniquer ?** Une fonction factory doit renvoyer une erreur dès lors que la validation dépend d'une entrée à l'exécution (configuration fournie par l'utilisateur, variables d'environnement, données externes). La panic est réservée aux erreurs de programmation, c'est-à-dire aux situations qui trahissent un bug, comme passer un pointeur nil là où le contrat de l'API l'interdit. La bibliothèque standard suit cette convention : `os.Open` renvoie une erreur, tandis que `regexp.MustCompile` panique parce qu'elle attend un motif constant à la compilation. **Q : En quoi le pattern Functional Options améliore-t-il l'évolution d'une API ?** L'ajout d'une nouvelle fonction `WithXxx` est un changement non cassant. Les appelants existants continuent de fonctionner sans modification. Cela contraste avec les structs de configuration, où l'ajout d'un champ obligatoire casse tous les sites d'appel, ou avec les paramètres positionnels, où réordonner les arguments introduit des bugs. **Q : Qu'est-ce qui distingue les interfaces Go de celles de Java ou C# ?** Les interfaces Go sont satisfaites implicitement. Un type implémente une interface simplement en disposant des méthodes requises, sans déclaration `implements`. Cela permet le principe « accepter des interfaces, renvoyer des structs » : définir des interfaces étroites au point d'appel, et non au point d'implémentation. Le résultat est un [couplage plus faible et une meilleure testabilité](/technologies/go/interview-questions/testing). > **Schéma d'entretien** > > Les recruteurs demandent souvent aux candidats de refactoriser une dépendance concrète en interface. Le test : le candidat sait-il identifier l'ensemble minimal de méthodes nécessaires et l'extraire côté consommateur plutôt que côté producteur ? **Q : En quoi un Observer basé sur les channels diffère-t-il d'un Observer basé sur les callbacks ?** Les channels découplent le producteur et l'abonné à la fois dans le temps et dans l'espace. Le producteur ne détient aucune référence vers les fonctions des abonnés : il envoie vers des channels. Les abonnés peuvent traiter les événements à leur propre rythme grâce aux channels bufferisés. L'instruction `select` permet la gestion des délais d'attente, l'annulation via `context` et le multiplexage de plusieurs sources d'événements, autant de capacités absentes des callbacks. > **Piège fréquent** > > Oublier de fermer les channels des abonnés provoque des fuites de goroutines. Chaque `Subscribe` doit avoir un `Unsubscribe` correspondant qui ferme le channel et le retire du bus. ## Conclusion - Le pattern Functional Options remplace les builders et les structs de configuration par une API propre et extensible qui ne casse jamais les appelants existants - La stratégie en Go se traduit par du polymorphisme basé sur les interfaces : définir l'interface côté consommateur, pas côté fournisseur - Les fonctions factory imposent des invariants que l'initialisation par valeur zéro ne peut garantir ; renvoyer des erreurs pour les entrées à l'exécution, ne paniquer que pour les bugs de programmation - L'Observer basé sur les channels exploite les goroutines et `select` pour une livraison d'événements concurrente et découplée, sans chaînes de callbacks - Les chaînes de middleware HTTP se composent via la signature `func(http.Handler) http.Handler`, identique dans la bibliothèque standard et les routeurs tiers - L'embedding de structs fournit de la délégation, pas de l'héritage : comprendre cette distinction sépare les développeurs qui maîtrisent Go de ceux qui transcrivent verbatim les patterns Java - Réussir un entretien exige de démontrer une adaptation idiomatique des patterns au système de types de Go, et non une récitation scolaire des définitions du Gang of Four --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/fr/blog/go/go-design-patterns-essential-patterns-interview