Aller au contenu
Développement medium

Charger une configuration YAML et TOML en Go

12 min de lecture

Go

Coder les réglages en dur fige un outil ; charger une configuration typée depuis un fichier le rend adaptable sans le recompiler. Ce guide vous apprend à décrire votre configuration dans une structure Go annotée de struct tags, à la remplir depuis un fichier YAML (gopkg.in/yaml.v3) ou TOML (github.com/BurntSushi/toml), à poser des valeurs par défaut pour les champs absents, à valider le résultat, et à laisser des drapeaux de ligne de commande écraser certaines valeurs. Public visé : vous connaissez les structures et les modules. Exemples testés sur Go 1.26, avec yaml.v3 v3.0.1 et BurntSushi/toml v1.6.0.

  • Décrire une configuration dans une structure avec des struct tags.
  • Charger un fichier YAML et un fichier TOML dans cette même structure.
  • Poser des valeurs par défaut et valider les champs manquants.
  • Écraser la configuration avec des drapeaux de ligne de commande.

Le point de départ est une structure Go qui reflète l'arborescence de votre fichier. Chaque champ porte un struct tag qui indique la clé correspondante dans le fichier. Un même champ peut déclarer plusieurs tags, ici yaml et toml, pour être lisible depuis les deux formats :

type Serveur struct {
Hote string `yaml:"hote" toml:"hote"`
Port int `yaml:"port" toml:"port"`
}
type Config struct {
Serveur Serveur `yaml:"serveur" toml:"serveur"`
LogLevel string `yaml:"log_level" toml:"log_level"`
}

Deux règles conditionnent le bon fonctionnement du décodage. D'abord, les champs doivent être exportés (initiale majuscule) : les bibliothèques utilisent la réflexion, qui ne voit pas les champs privés. Ensuite, le tag mappe la clé exacte du fichier au champ, ce qui vous laisse nommer vos champs à la mode Go (LogLevel) tout en lisant une clé en snake_case (log_level).

La bibliothèque de référence est gopkg.in/yaml.v3. On lit le fichier avec os.ReadFile, puis yaml.Unmarshal remplit la structure passée par pointeur :

import "gopkg.in/yaml.v3"
func chargerYAML(chemin string) (Config, error) {
var cfg Config
data, err := os.ReadFile(chemin)
if err != nil {
return cfg, err
}
if err := yaml.Unmarshal(data, &cfg); err != nil {
return cfg, err
}
return cfg, nil
}

Avec ce fichier config.yaml :

serveur:
hote: 0.0.0.0
port: 9090
log_level: debug

La structure obtenue reflète fidèlement le contenu :

YAML complet : {Serveur:{Hote:0.0.0.0 Port:9090} LogLevel:debug}

Le format TOML se lit avec github.com/BurntSushi/toml. Sa fonction toml.DecodeFile enchaîne la lecture et le décodage en un seul appel, et renvoie en prime des métadonnées utiles :

import "github.com/BurntSushi/toml"
func chargerTOML(chemin string) (Config, error) {
var cfg Config
if _, err := toml.DecodeFile(chemin, &cfg); err != nil {
return cfg, err
}
return cfg, nil
}

Avec ce config.toml :

log_level = "warn"
[serveur]
hote = "10.0.0.1"
port = 7070

Le résultat remplit la même structure Config que le YAML :

TOML : {Serveur:{Hote:10.0.0.1 Port:7070} LogLevel:warn}

Un fichier de configuration réel est souvent partiel : l'utilisateur ne renseigne que ce qu'il veut changer. L'astuce est de pré-remplir la structure avec vos valeurs par défaut avant l'Unmarshal. Les décodeurs ne touchent qu'aux champs présents dans le fichier, laissant les autres intacts :

func valeursParDefaut() Config {
return Config{
Serveur: Serveur{Hote: "127.0.0.1", Port: 8080},
LogLevel: "info",
}
}
func chargerYAML(chemin string) (Config, error) {
cfg := valeursParDefaut() // <- défauts d'abord
data, err := os.ReadFile(chemin)
if err != nil {
return cfg, err
}
return cfg, yaml.Unmarshal(data, &cfg)
}

Avec un fichier qui ne précise que l'hôte :

serveur:
hote: 192.168.1.10

Le port et le niveau de log conservent leurs valeurs par défaut, seule l'hôte est remplacée :

YAML partiel : {Serveur:{Hote:192.168.1.10 Port:8080} LogLevel:info}

Un décodage réussi ne garantit pas une configuration cohérente : un port peut être hors bornes, un champ requis peut manquer. Ajoutez une méthode de validation appelée juste après le chargement, qui renvoie une erreur explicite :

func (c Config) valider() error {
if c.Serveur.Port < 1 || c.Serveur.Port > 65535 {
return fmt.Errorf("port invalide: %d", c.Serveur.Port)
}
return nil
}
message: port invalide: 70000

Cette étape est d'autant plus importante qu'une clé absente laisse le champ à sa valeur nulle ("", 0, nil) sans provoquer d'erreur. La validation est votre filet de sécurité contre les configurations incomplètes.

En pratique, on veut souvent qu'un drapeau de ligne de commande prenne le pas sur le fichier, par exemple pour changer un port le temps d'un test. L'astuce est d'utiliser la valeur issue de la configuration comme valeur par défaut du drapeau : le drapeau ne l'emporte que s'il est réellement fourni.

port := flag.Int("port", cfg.Serveur.Port, "port du serveur")
flag.Parse()
cfg.Serveur.Port = *port

Sans drapeau, la valeur du fichier est conservée ; avec -port 3000, elle est remplacée :

sans flag : port effectif: 8080
avec -port 3000: port effectif: 3000

Le drapeau ne l'emporte que s'il est fourni, sinon la valeur du fichier reste en place.

Compléter avec l'environnement, gérer l'absence de fichier

Section intitulée « Compléter avec l'environnement, gérer l'absence de fichier »

Deux besoins achèvent ce socle en production. D'abord, un déploiement conteneurisé préfère souvent surcharger un réglage par une variable d'environnement plutôt que d'éditer un fichier. Lisez-la avec os.Getenv après le chargement, et n'écrasez la valeur que si elle est définie :

func appliquerEnv(cfg *Config) {
if v := os.Getenv("APP_PORT"); v != "" {
if p, err := strconv.Atoi(v); err == nil {
cfg.Serveur.Port = p
}
}
}

Ensuite, l'absence de fichier ne doit pas être fatale : un outil doit pouvoir démarrer sur ses seuls défauts. Distinguez le cas « fichier absent » des vraies erreurs de lecture avec errors.Is(err, os.ErrNotExist) :

data, err := os.ReadFile(chemin)
if errors.Is(err, os.ErrNotExist) {
return valeursParDefaut(), nil // pas de fichier: on garde les défauts
}
fichier absent -> defaut: 8080
après APP_PORT=5000 : 5000

Vous obtenez alors la hiérarchie complète que suivent la plupart des outils : défauts, puis fichier, puis variables d'environnement, puis drapeaux, chaque source affinant la précédente.

Le chargement de configuration réserve deux pièges silencieux, qui reviennent constamment dans les questions des débutants Go. Aucun ne provoque d'erreur par défaut : votre programme démarre, mais avec une valeur inattendue.

SymptômeCauseCorrection
Un champ reste toujours à zérochamp non exporté (initiale minuscule) : la réflexion l'ignoreMettre une majuscule au nom du champ
Une clé du fichier n'a aucun effetfaute de frappe dans la clé, silencieusement ignoréeDécodage strict (voir ci-dessous) ; valider les champs requis
Une valeur écrase un défaut avec 0/""la clé est présente mais vide dans le fichierRetirer la clé du fichier, ou valider les bornes
Panique au chargementfichier absent ou illisible, erreur non géréeTraiter l'erreur de os.ReadFile / DecodeFile

Le piège le plus courant est la faute de frappe dans une clé. Écrire prot: au lieu de port: ne déclenche rien : le champ reste à 0 et le programme tourne avec une valeur fantôme. Chaque bibliothèque offre un mode strict qui transforme ce silence en erreur. Côté YAML, un décodeur avec KnownFields(true) refuse toute clé inconnue :

dec := yaml.NewDecoder(bytes.NewReader(data))
dec.KnownFields(true)
err := dec.Decode(&cfg)
// yaml: unmarshal errors: line 1: field prot not found in type main.Config

Côté TOML, toml.Decode renvoie des métadonnées dont Undecoded() liste les clés du fichier qui n'ont trouvé aucun champ :

md, _ := toml.Decode(data, &cfg)
fmt.Println(md.Undecoded()) // [prot]

En développement, activez ce mode strict pour repérer les fautes de frappe immédiatement. En production, restez tolérant si vous voulez accepter des clés futures, mais validez toujours les réglages critiques après le chargement.

Assemblez les briques. Cherchez la solution avant d'ouvrir la réponse.

Complétez la structure Config avec un champ MaxConnexions int (clé max_connexions), une valeur par défaut de 100, et une validation qui refuse une valeur négative.

Chargez ce YAML partiel, qui ne fixe que le niveau de log :

log_level: debug

Le résultat attendu doit garder MaxConnexions à 100 et LogLevel à debug.

  • La configuration se décrit dans une structure Go dont les champs, exportés, portent des struct tags (yaml:"...", toml:"...").
  • yaml.Unmarshal (gopkg.in/yaml.v3) et toml.DecodeFile (BurntSushi/toml) remplissent la même structure passée par pointeur.
  • Poser les valeurs par défaut avant l'Unmarshal préserve les champs que le fichier ne mentionne pas.
  • Une clé absente ou mal orthographiée laisse le champ à zéro sans erreur : validez après chargement.
  • Le mode strict (KnownFields(true) en YAML, Undecoded() en TOML) transforme les fautes de frappe en erreurs.
  • Utiliser la valeur du fichier comme défaut d'un drapeau donne la hiérarchie défauts, fichier, drapeaux.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn