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.

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é.
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 et désormais standard dans l'écosystème Go, utilise des arguments variadiques pour configurer une struct.
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.
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.
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.
Prêt à réussir tes entretiens Go ?
Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.
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 de Go, en évitant le plat de spaghettis de callbacks.
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.
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, 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. Les candidats qui confondent embedding et héritage révèlent une compréhension superficielle du système de types de 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é.
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.
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.
Prêt à réussir tes entretiens Go ?
Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.
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
selectpour 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
Passe à la pratique !
Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.
Tags
Partager
Articles similaires

Questions d'entretien Go 1.26 : Green Tea GC, go fix et nouvelles fonctionnalités
Guide complet des questions d'entretien sur Go 1.26 couvrant le nouveau garbage collector Green Tea, les optimisations de pile, l'outil go fix et les améliorations syntaxiques.

Entretien technique Go : Goroutines, Channels et Concurrence
Questions d'entretien Go sur les goroutines, channels et patterns de concurrence. Exemples de code, pieges courants et reponses de niveau expert pour preparer les entretiens techniques Go en 2026.

Top 25 questions d'entretien Go : Guide complet pour développeurs
Préparez vos entretiens Go avec les 25 questions les plus posées. Goroutines, channels, interfaces, concurrence et patterns avancés avec exemples de code.