Le système de templates de Go, souvent appelé go template, sert à séparer proprement les données et la présentation sans alourdir un backend. Je m’en sers surtout quand je veux générer du HTML, du texte, un email ou un fichier de configuration avec une logique lisible et peu de dépendances. Dans cet article, je vais expliquer comment il fonctionne, quand choisir `text/template` ou `html/template`, comment structurer des templates maintenables, et quels pièges j’évite systématiquement en revue de code.
Ce moteur transforme des données en texte lisible, sûr et facile à maintenir
- `text/template` sert au texte brut, tandis que `html/template` ajoute un échappement contextuel pour le HTML.
- La syntaxe repose sur `{{ ... }}`, le point `.` pour les données courantes et les pipelines pour garder les actions courtes.
- Pour une page web, je recommande presque toujours `html/template`; pour les emails, les exports ou la génération de fichiers, `text/template` reste très adapté.
- Les fonctions personnalisées doivent être enregistrées avant le parsing, sinon elles ne sont pas disponibles au moment de l’analyse.
- `missingkey=error` est un réglage simple qui évite beaucoup d’erreurs silencieuses en développement et en production.
Ce que fait vraiment le moteur de templates de Go
Le point clé est simple: un template Go n’est pas un mini-framework de présentation, c’est un moteur d’assemblage entre une structure de données et une sortie finale. Le texte hors balises est copié tel quel, et tout ce qui se trouve entre `{{` et `}}` devient une action qui lit des champs, appelle une fonction, exécute une condition ou parcourt une collection. La documentation de Go insiste sur ce découplage: le template reçoit des données, puis il navigue dans la structure via le point `.`.
Dans la pratique, je l’utilise pour trois cas très différents: un site web généré côté serveur, un email transactionnel, ou un fichier texte à produire automatiquement. Le même mécanisme reste utile tant que la sortie finale est du texte et que la logique métier reste dans le code Go, pas dans le template. C’est précisément ce qui rend l’outil sobre et durable. Une fois ce modèle en tête, la syntaxe devient beaucoup moins intimidante.

Comprendre la syntaxe sans se perdre
Le langage de template est compact, mais il faut assimiler quelques repères: `.` pour les données courantes, `$` pour une variable, `if` pour une condition, `range` pour itérer, `with` pour réorienter le point courant, et les pipelines pour chaîner les opérations de façon lisible. Je conseille de commencer par ces briques avant d’ajouter des fonctions personnalisées, parce qu’elles couvrent déjà une grande partie des besoins quotidiens.
| Construction | Rôle | Quand je l’utilise |
|---|---|---|
{{.Title}} |
Lit un champ ou une clé dans les données. | Pour afficher une valeur simple, sans logique supplémentaire. |
{{if .Published}}...{{end}} |
Affiche un bloc seulement si la condition est vraie. | Pour masquer une section vide ou un état non prêt. |
{{range .Tags}}...{{end}} |
Parcourt une tranche, une liste ou une map. | Pour générer des items de menu, des tags ou des lignes de tableau. |
{{with .Author}}...{{end}} |
Change le point courant pour simplifier l’accès aux champs. | Quand une sous-structure est présente mais facultative. |
{{printf "%s" .Body}} |
Appelle une fonction ou un pipeline. | Pour formater une valeur sans encombrer le Go côté serveur. |
Ce que je trouve le plus utile, c’est la lisibilité du pipeline. Au lieu d’écrire de la logique imbriquée, on peut enchaîner des étapes courtes et explicites. Un template comme {{.Title | printf "%q"}} dit immédiatement ce qu’il fait: il récupère la valeur, puis la formate. C’est simple, mais très efficace quand le projet grandit. Reste à choisir le bon package selon la sortie visée.
Choisir entre texte brut et HTML sécurisé
Go fournit deux paquets proches, mais leur objectif n’est pas le même. text/template rend du texte brut et n’ajoute aucun échappement automatique; html/template reprend la même API, mais ajoute un échappement contextuel pensé pour le HTML. Pour toute page web, la règle que je garde est presque binaire: si la sortie est du HTML, j’utilise html/template; sinon je pars sur text/template.
| Critère | text/template |
html/template |
|---|---|---|
| Sortie cible | Texte, emails, fichiers, logs, Markdown, génération de code. | HTML servi à un navigateur. |
| Échappement | Aucun échappement automatique. | Échappement contextuel selon HTML, attribut, URL, CSS ou JS. |
| Risque principal | Injection si on l’emploie pour produire du HTML sans protection. | Mauvaise confiance dans des données ou des types “safes”. |
| Cas typique | Mail transactionnel, export texte, fichier de config. | Page serveur, fragment de vue, formulaire, widget HTML. |
| Mon réflexe | Par défaut pour tout ce qui n’est pas du HTML. | Dès que le navigateur est le consommateur final. |
Le vrai intérêt de html/template n’est pas seulement l’échappement du texte. Il comprend aussi les contextes HTML, JS, CSS et URI, donc il adapte la sortie à l’endroit où la donnée apparaît. C’est pratique, mais je ne traite jamais ça comme un remplaçant de la validation serveur: un champ mal formé reste un problème métier, même s’il est correctement encodé à l’affichage. Le vrai sujet devient alors l’organisation du code.
Organiser des templates maintenables dans un vrai projet
Quand un projet grossit, le sujet n’est plus la syntaxe. Le vrai enjeu devient le chargement, le nommage et la réutilisation. Je préfère charger tous les templates au démarrage, les nommer clairement, puis rendre des sous-templates avec ExecuteTemplate plutôt que de concaténer du HTML à la main dans les handlers.
-
ParseFilesconvient quand vous avez quelques fichiers locaux et un chargement direct. -
ParseGlobdevient utile quand vous organisez plusieurs vues par motif de fichiers. -
ParseFSest très pratique si vous embarquez les templates avecembedpour un déploiement autonome. -
ExecuteTemplatepermet de cibler un layout ou un fragment nommé sans ambiguïté. -
Funcsdoit être appelé avant le parsing si vous voulez utiliser des helpers commeupper,truncateou un formateur de devise.
//go:embed templates/*.html
var templatesFS embed.FS
var tpl = template.Must(
template.New("base").
Funcs(template.FuncMap{
"upper": strings.ToUpper,
}).
Option("missingkey=error").
ParseFS(templatesFS, "templates/*.html"),
)
{{define "base"}}
{{template "content" .}}
{{end}}
{{define "content"}}
{{.Title}}
{{.Body}}
{{end}}
Cette structure évite la duplication et garde le rendu lisible. Elle a aussi un autre avantage: on peut tester plus facilement les layouts, les blocs de contenu et les helpers sans relire du HTML généré à la main dans un handler. Même bien structurés, les templates ont pourtant des pièges qu’il vaut mieux verrouiller tôt.
Les pièges qui ressortent en production
La plupart des bugs que je vois autour des templates ne viennent pas de la syntaxe elle-même, mais de détails d’intégration. Le premier est silencieux: une clé manquante dans une map ou une structure affiche parfois ou une valeur vide au lieu de casser franchement. En développement et en test, je préfère donc missingkey=error, parce qu’il transforme un oubli en erreur visible.
| Symptôme | Cause probable | Réflexe utile |
|---|---|---|
dans le rendu |
Champ absent dans les données. | Activer missingkey=error pendant le dev et les tests. |
| HTML cassé ou non protégé | Mauvais package ou mauvais contexte d’usage. | Utiliser html/template pour toute sortie HTML. |
| Fonction introuvable |
Funcs appelé après le parsing. |
Enregistrer les helpers avant Parse. |
| Erreur ignorée |
Parse ou Execute non vérifié. |
Traiter chaque retour d’erreur explicitement. |
| Rendu partiel | Une erreur est survenue après le début de l’écriture. | Ne pas supposer qu’un échec ne produit rien. |
Le second piège est plus architectural: vouloir faire porter au template trop de logique. Je garde les boucles, les conditions simples et le formatage léger, mais je laisse la transformation métier au code Go. La documentation de Go rappelle aussi un point important: le moteur suppose les auteurs de templates dignes de confiance, alors que les données passées à l’exécution ne le sont pas forcément. Autrement dit, je n’ouvre jamais la porte à des templates modifiables par des utilisateurs non fiables, et je n’emploie les types “sûrs” comme template.HTML ou template.JS qu’avec un contenu réellement maîtrisé. À ce stade, la question n’est plus seulement technique, elle devient architecturale.
Le niveau d’abstraction que je recommande pour un backend Go durable
Dans un backend classique, j’utilise les templates tant que l’interface reste surtout déclarative: pages serveur, mails, PDF simple, exports textuels. Dès que l’écran devient très interactif, avec état client, interactions riches et composants réutilisables complexes, je bascule plutôt vers une séparation plus nette entre API Go et frontend dédié. Les templates Go restent alors excellents pour la couche de rendu, mais ils ne doivent pas porter tout le poids du produit.
- Je prépare les données côté Go avant de les envoyer au template, au lieu de disperser des calculs dans le HTML.
- Je centralise le chargement des fichiers au démarrage pour détecter les erreurs tôt.
- Je garde les helpers petits, purs et prévisibles, sinon ils deviennent une source de dette silencieuse.
- Je teste les templates importants avec des cas réels, surtout ceux qui touchent la sécurité ou les parcours critiques.
- J’utilise l’embarquement des fichiers quand je veux un déploiement simple et reproductible.