Comment gérer les paramètres optionnels en Go.
Posté le 15 novembre 2025 • 7 min de lecture • 1 361 motsGo ne propose pas de paramètres optionnels ni de surcharge de fonctions. Voici les patterns idiomatiques, leurs avantages, leurs limites.

Dans la plupart des langages modernes, il est possible de définir des valeurs par défaut dans les fonctions ou de les surcharger pour couvrir plusieurs cas d’usage.
Go, lui, ne propose ni paramètres optionnels, ni surcharge, ni valeurs par défaut dans les signatures de fonction.
Pourtant, les besoins restent les mêmes : créer des APIs lisibles, stables et capables d’évoluer sans casser les utilisateurs.
Cet article présente toutes les approches idiomatiques pour gérer les paramètres optionnels en Go, quand les utiliser, comment éviter les pièges et comment concevoir une API propre — avec des tableaux de décision et des exemples concrets inspirés de projets DevOps, Kubernetes ou AWS.
Sans paramètres optionnels natifs, un code Go naïf devient vite illisible :
NewAPIClient("https://service", token, 0, 0, nil, false)Ce genre d’appel force les développeurs à mémoriser la signification de chaque argument, crée des ambiguïtés, et rend l’évolution de l’API difficile. C’est pour cette raison que toute l’écosystème Go a convergé vers quelques patterns simples et efficaces pour représenter des options de manière claire.
L’objectif n’est pas seulement d’avoir du code fonctionnel, mais une API:
Ce tableau présente les recommandations pratiques selon les cas courants rencontrés en Go :
| Situation | Pattern recommandé |
|---|---|
| 1–2 paramètres optionnels simples | Variantes de fonctions |
| Plusieurs options, API standard | Struct d’options |
| Beaucoup d’options, API publique ou évolutive | Functional Options (...Option) |
Zéro-valeur ambiguë (0, false, "") | Functional Options ou pointeurs |
| Besoin tri-état (défaut / vrai / faux) | Pointeurs sur scalaires (*bool, *int) |
| Ressource facultative (ex : TLS) | Pointeur accepté à nil |
Ces approches permettent d’obtenir du code clair tout en évitant l’explosion combinatoire des signatures.
Lorsque l’on n’a qu’un paramètre optionnel ou deux cas d’usage bien distincts, la forme la plus simple reste de créer deux fonctions explicites.
func NewServer(addr string) (*Server, error) { /* ... */ }
func NewServerTLS(addr string, tlsCfg *tls.Config) (*Server, error) { /* ... */ }Cette approche est très lisible et ne nécessite aucune abstraction supplémentaire. La fonction elle-même encode l’option (NewServer vs NewServerTLS). Elle devient cependant limitée si les options se multiplient : ajouter plus de variantes entraîne rapidement une complexité inutile.
La méthode la plus idiomatique en Go consiste à regrouper toutes les options dans une structure passée à la fonction. Chaque champ utilise sa zéro-valeur comme valeur par défaut, ce qui rend l’API simple et évolutive.
Exemple
type ClientOptions struct {
Timeout time.Duration // 0 = défaut
Retries int // 0 = défaut
Logger *log.Logger // nil = log.Default()
InsecureTLS bool // false = par défaut
}
func NewAPIClient(baseURL, token string, opts ClientOptions) (*APIClient, error) {
if opts.Timeout == 0 {
opts.Timeout = 5 * time.Second
}
if opts.Retries == 0 {
opts.Retries = 3
}
if opts.Logger == nil {
opts.Logger = log.Default()
}
return &APIClient{/* ... */}, nil
}Usage
client, _ := NewAPIClient("https://api.service", token, ClientOptions{
Timeout: 2 * time.Second,
InsecureTLS: true,
})Cette approche est lisible, simple, intuitive pour l’utilisateur, compatible avec les outils d’auto-complétion, et elle supporte parfaitement l’évolution de l’API : on peut ajouter un champ dans la struct sans casser les appels existants.
La seule limite apparaît lorsque la zéro-valeur a une signification métier. Par exemple, Timeout = 0 pourrait vouloir dire “pas de timeout” — et non “utilise la valeur par défaut”. Dans ce cas, on utilise un autre pattern.
...Option)
Ce pattern est utilisé par Kubernetes, Docker, Prometheus, Terraform, etc. Il consiste à transformer les options en fonctions configuratrices, ce qui permet d’obtenir une API très lisible et complètement extensible.
Exemple complet
type Option func(*config)
type config struct {
timeout time.Duration
retries int
logger *log.Logger
insecureTLS bool
}
func WithTimeout(d time.Duration) Option {
return func(c *config) { c.timeout = d }
}
func WithRetries(n int) Option {
return func(c *config) { c.retries = n }
}
func WithLogger(l *log.Logger) Option {
return func(c *config) { c.logger = l }
}
func WithInsecureTLS() Option {
return func(c *config) { c.insecureTLS = true }
}
func NewAPIClient(baseURL, token string, opts ...Option) (*APIClient, error) {
cfg := config{
timeout: 5 * time.Second,
retries: 3,
logger: log.Default(),
}
for _, o := range opts {
o(&cfg)
}
return &APIClient{/* ... */}, nil
}Usage
client, _ := NewAPIClient(
"https://api.xyz",
token,
WithTimeout(1*time.Second),
WithRetries(4),
WithInsecureTLS(),
)Ce pattern est idéal pour les APIs publiques, exposées et destinées à durer longtemps. Il évite les ambiguïtés tout en restant élégant à l’appel. (WithTimeout(…), WithLogger(…)
Certaines options nécessitent de distinguer 3 états:
Dans ce cas, les structures d’options et les functional options doivent utiliser des pointeurs sur scalaires.
type ClientOptions struct {
EnableCache *bool // nil = défaut, true/false = explicite
}
func boolPtr(b bool) *bool { return &b }Usage
NewAPIClient("https://api", token, ClientOptions{
EnableCache: boolPtr(false),
})C’est particulièrement utile dans les projets DevOps, Kubernetes et AWS où les paramètres booléens doivent parfois représenter un état complexe.
Accepter nil pour des pointeurs est normal en Go. En revanche, avec les interfaces, un “typed nil” peut provoquer des comportements inattendus.
nil d’interface n’est pas toujours nil ?
Une interface Go contient deux informations :
Elle n’est réellement nil que si les deux sont nil :
dynamicType = nil, dynamicValue = nil
Le piège survient lorsqu’on stocke dans une interface un pointeur nil, par exemple :
var buf *bytes.Buffer = nil
var w io.Writer = bufIci, w vaut :
(dynamicType = *bytes.Buffer, dynamicValue = nil)L’interface n’est pas nil, puisqu’elle contient un type.
fmt.Println(w == nil) // falseConséquence : risque de panic
Comme l’interface n’est pas considérée nil, du code comme :
if w != nil {
w.Write([]byte("hello"))
}donnera un panic :
panic: runtime error: invalid memory address or nil pointer dereferenceParce que Go tente d’appeler la méthode Write sur une valeur interne nil.
Ce qu’il ne faut pas faire
Utiliser nil pour une interface comme signal métier
Se reposer sur w == nil pour tester si un writer / logger / output existe
La bonne pratique
func Do(w io.Writer) {
if w == nil {
w = io.Discard // writer neutre et sans danger
}
w.Write([]byte("..."))
}Ainsi, on évite :
w == nil trompeursEn Go :
nil pour un pointeur est autorisénil pour une interface peut être non-nil et causer un panicio.Discard, logger par défaut, etc.) plutôt que de compter sur des tests de nil avec les interfaces.| Nombre d’options | Ambiguïté zéro-valeur | API publique ? | Pattern recommandé |
|---|---|---|---|
| 0–2 options | non | non | Variantes de fonctions |
| 3–10 | non | non | Struct d’options |
| 3–∞ | oui | non | Pointeurs dans struct |
| 3–∞ | oui ou non | oui | Functional Options |
| Tri-état | oui | oui ou non | Pointeurs sur scalaires |
Même si Go ne propose pas de paramètres optionnels, il offre un ensemble de patterns élégants et efficaces pour modéliser des APIs flexibles et robustes.
La clé est de choisir le bon outil selon la situation :
En appliquant ces principes, votre code devient non seulement plus lisible, mais aussi plus stable, plus maintenable et plus idiomatique Go — ce qui facilite la collaboration et les évolutions à long terme.