Skip to content

template

Documentation officielle : template package - text/template - Go Packages

Dans la pratique, nous utilisons souvent la fonction fmt.Sprintf pour formater des chaînes, mais elle ne convient que pour les petites chaînes. De plus, elle nécessite d'utiliser des verbes de formatage pour spécifier les types, ne permet pas de nommer les paramètres, et ne supporte pas les situations complexes. C'est ce que le moteur de template doit résoudre. Par exemple, pour les pages HTML statiques directement attachées au backend, un moteur de template est nécessaire. La communauté propose d'excellentes bibliothèques de moteurs de template tiers comme pongo2, sprig, jet. Cependant, cet article présente le moteur de template intégré de Go : text/template. En développement réel, on utilise généralement html/template, qui est basé sur le premier et a été amélioré pour la sécurité HTML. En général, le premier suffit, mais si vous traitez des templates HTML, il est recommandé d'utiliser le second pour plus de sécurité.

Démarrage rapide

Voici un exemple simple d'utilisation du moteur de template

go
package main

import (
  "fmt"
  "os"
  "text/template"
)

func main() {
  tmpl := `This is the first template string, {{ .message }}`

  te, err := template.New("texTmpl").Parse(tmpl)
  if err != nil {
    fmt.Println(err)
    return
  }

  data := map[string]any{
    "message": "hello world!",
  }
  execErr := te.Execute(os.Stdout, data)
  if execErr != nil {
    fmt.Println(err)
  }
}

La sortie du code ci-dessus est

This is the first template string, hello world!

Dans l'exemple, tmpl est une chaîne de template, et {{ .message }} est un paramètre de template. D'abord, on analyse la chaîne de template avec la méthode *Template.Parse

go
func (t *Template) Parse(text string) (*Template, error)

Après une analyse réussie, on applique les données data au template avec la méthode *Template.Execute, puis on sort vers le Writer passé, ici os.Stdout.

go
func (t *Template) Execute(wr io.Writer, data any) error

Dans l'utilisation future du moteur de template, ce sera toujours ces trois étapes :

  1. Obtenir le template
  2. Analyser le template
  3. Appliquer les données au template

On voit que l'utilisation du moteur de template est assez simple. Ce qui est un peu plus complexe, c'est la syntaxe du template, qui est le contenu principal de cet article.

Syntaxe du template

Paramètres

Go utilise deux paires d'accolades {{ }} pour indiquer un paramètre de template dans le template. Le . représente l'objet racine, qui est la data passée. Comme pour accéder aux membres d'un type, on utilise le . suivi du nom de la variable pour accéder à la valeur correspondante dans le template, par exemple

{{ .data }}

À condition que la variable membre correspondante existe, sinon une erreur sera signalée. Pour la data passée, c'est généralement une structure ou une map, mais cela peut aussi être un type de base comme un nombre ou une chaîne. Dans ce cas, le . représente l'objet racine lui-même. Dans les accolades, on n'est pas obligé d'accéder à l'objet racine pour obtenir une valeur, on peut aussi utiliser des littéraux de types de base, par exemple

{{ 1 }}
{{ 3.14 }}
{{ "jack" }}

Quel que soit le type, la forme finale sera obtenue par fmt.Sprintf("%s", val). Voyons l'exemple suivant.

go
func main() {
  out := os.Stdout

  tmpl := "data-> {{ . }}\n"

  datas := []any{
    "hello world!",
    6379,
    3.1415926,
    []any{1, "2*2", 3.6},
    map[string]any{"data": "hello world!"},
    struct {
      Data string
    }{Data: "hello world!"},
  }

  for _, data := range datas {
    err := ExecTmpl(out, tmpl, data)
    if err != nil {
      panic(err)
    }
  }
}

func ExecTmpl(writer io.Writer, tmpl string, data any) error {
  parsedTmpl, err := template.New("template").Parse(tmpl)
  if err != nil {
    return err
  }
  return parsedTmpl.Execute(writer, data)
}

Sortie

data-> hello world!
data-> 6379
data-> 3.1415926
data-> [1 2*2 3.6]
data-> map[data:hello world!]
data-> {hello world!}

On voit que la forme de sortie est identique à l'utilisation directe de fmt.Sprintf. Pour les structures et les maps, on peut accéder à leurs valeurs par le nom du champ, comme montré ci-dessous

go
func main() {
  out := os.Stdout

  tmpl := "data-> {{ .Data }}\n"

  datas := []any{
    map[string]any{"Data": "hello world!"},
    struct {
      Data string
    }{Data: "hello world!"},
  }

  for _, data := range datas {
    err := ExecTmpl(out, tmpl, data)
    if err != nil {
      panic(err)
    }
  }
}

Sortie

data-> hello world!
data-> hello world!

Pour les tranches et les map, bien qu'il n'y ait pas de syntaxe spécifique pour accéder à un index particulier, on peut le faire par un appel de fonction, comme montré ci-dessous

go
func main() {
  out := os.Stdout

  tmpl := "data-> {{ index . 1}}\n"

  datas := []any{
    []any{"first", "second"},
    map[int]any{1: "first"},
  }

  for _, data := range datas {
    err := ExecTmpl(out, tmpl, data)
    if err != nil {
      panic(err)
    }
  }
}

Sortie

data-> second
data-> first

Pour les tranches multidimensionnels, on peut accéder aux valeurs d'index correspondantes de la manière suivante, équivalent à s[i][j][k]

{{ index . i j k }}

Pour les structures ou maps imbriqués, on peut utiliser .k1.k2.k3 pour accéder, par exemple

{{ .person.father.name }}

Lors de l'utilisation des paramètres de template, on peut ajouter un symbole - avant et après le paramètre pour éliminer les espaces avant et après. Voyons un exemple

go
func main() {
  out := os.Stdout

  tmpl := `{{ .x }} {{ - .op - }} {{ .y }}`

  datas := []any{
    map[string]any{"x": "10", "op": ">", "y": "2"},
  }

  for _, data := range datas {
    err := ExecTmpl(out, tmpl, data)
    if err != nil {
      panic(err)
    }
  }
}

Normalement, le résultat devrait être 10 > 2, mais comme on a ajouté - avant et après le paramètre op, les espaces avant et après seront éliminés, donc la sortie réelle sera

10>2

Notez que dans les accolades, le symbole - doit être séparé du paramètre par un espace. Le format doit être {{- . -}}. Dans l'exemple, j'ai ajouté des espaces supplémentaires de chaque côté {{ - . - }} simplement parce que je trouve cela plus lisible, mais il n'y a pas de restriction syntaxique à cela.

Commentaires

La syntaxe du template supporte les commentaires, qui ne seront pas générés dans le template final. La syntaxe est la suivante

{{/* this is a comment */}}

Les symboles de commentaire /* et */ doivent être adjacents aux accolades, il ne doit pas y avoir d'autres caractères entre eux, sinon l'analyse échouera. Il n'y a qu'une seule exception, lors de l'élimination des espaces

{{- /* this is a comment */ -}}

Variables

On peut aussi déclarer des variables dans le template. Le symbole $ indique qu'il s'agit d'une variable, et on utilise := pour l'assignation, comme dans le code Go. Exemple

{{ $name := .Name }}
{{ $val := index . 1 }}
{{ $val := index .dict key }}
// assignation d'entier
{{ $numer := 1 }}
// assignation de nombre à virgule flottante
{{ $float := 1.234}}
// assignation de chaîne
{{ $name := "jack" }}

Pour l'utilisation ultérieure, on accède à la valeur de la variable par $ suivi du nom de la variable, par exemple

go
func main() {
  out := os.Stdout

  tmpl := `{{ $name := .name }} {{- $name }}`

  datas := []any{
    map[string]any{"name": "jack"},
  }

  for _, data := range datas {
    err := ExecTmpl(out, tmpl, data)
    if err != nil {
      panic(err)
    }
  }
}

Sortie

jack

Les variables doivent être déclarées avant d'être utilisées, sinon le message undefined variable apparaîtra, et elles doivent aussi être dans la portée.

Fonctions

La syntaxe du template elle-même n'est pas très riche, la plupart des fonctionnalités sont implémentées par des fonctions. Le format d'appel de fonction est le nom de la fonction suivi de la liste des paramètres, séparés par des espaces, comme suit

{{ funcname arg1 arg2 arg3 ... }}

Par exemple, la fonction index utilisée précédemment

{{ index .s 1 }}

La fonction eq pour comparer l'égalité

{{ eq 1 2 }}

Chaque *Template a une FuncsMap qui enregistre le mappage des fonctions

go
type FuncMap map[string]any

À la création du template, on obtient la table de mappage des fonctions par défaut depuis text/template.builtins. Voici toutes les fonctions intégrées

Nom de fonctionRôleExemple
andOpération ET{{ and true false }}
orOpération OU{{ or true false }}
notOpération NON{{ not true }}
eqÉgalité{{ eq 1 2 }}
neInégalité{{ ne 1 2 }}
ltInférieur à{{ lt 1 2 }}
leInférieur ou égal à{{ le 1 2 }}
gtSupérieur à{{ gt 1 2 }}
geSupérieur ou égal à{{ ge 1 2 }}
lenRetourne la longueur{{ len .slice }}
indexObtient l'élément à l'index spécifié{{ index . 0 }}
sliceTranche, équivalent à s[1:2:3]{{ slice . 1 2 3 }}
htmlÉchappement HTML{{ html .name }}
jsÉchappement js{{ js .name }}
printfmt.Sprint{{ print . }}
printffmt.Sprintf{{ printf "%s" .}}
printlnfmt.Sprintln{{ println . }}
urlqueryÉchappement url query{{ urlquery .query }}

En plus de celles-ci, il y a une fonction intégrée spéciale call, qui permet d'appeler directement une fonction passée dans la data lors de l'exécution Execute, par exemple avec le template suivant

{{ call .string 1024 }}

Données passées

go
map[string]any{
    "string": func(val any) string { return fmt.Sprintf("%v: 2048", val) },
}

Alors dans le template, cela générera

1024: 2048

C'est l'une des façons de personnaliser les fonctions. Cependant, il est généralement recommandé d'utiliser la méthode *Template.Funcs pour ajouter des fonctions personnalisées, car cette dernière a une portée globale et n'a pas besoin d'être liée à l'objet racine.

go
func (t *Template) Funcs(funcMap FuncMap) *Template

Le retour d'une fonction personnalisée a généralement deux valeurs, la première est la valeur à utiliser, la seconde est error. Par exemple, la fonction personnalisée suivante

go
template.FuncMap{
    "add": func(val any) (string, error) { return fmt.Sprintf("%v+1", val), nil },
}

Puis on l'utilise directement dans le template

{{ add 1024 }}

Le résultat est

1024 + 1

Pipeline

Ce pipeline est différent de chan. Dans la documentation officielle, on l'appelle pipeline. Toute opération qui peut produire des données est appelée pipeline. Les opérations de template suivantes sont des opérations de pipeline

{{ 1 }}
{{ eq 1 2 }}
{{ $name }}
{{ .name }}

Ceux qui connaissent Linux savent qu'il existe un opérateur de pipe |. Le template supporte aussi cette écriture. Les opérations de pipeline apparaissent fréquemment dans les templates, par exemple

{{ $name := 1 }}{{ $name | print | printf "%s+1=?" }}

Le résultat est

1+1=?

Dans les with, if, range suivants, on utilisera aussi fréquemment des pipelines.

with

L'instruction with permet de contrôler la portée des variables et de l'objet racine. Le format est le suivant

{{ with pipeline }}
  text
{{ end }}

with vérifie la valeur retournée par l'opération pipeline. Si la valeur est vide, le template text intermédiaire ne sera pas généré. Si vous voulez traiter le cas vide, vous pouvez utiliser with else, avec le format suivant

{{ with pipeline }}
  text1
{{ else }}
  text2
{{ end }}

Si l'opération pipeline retourne une valeur vide, alors le bloc else sera exécuté. Les variables déclarées dans l'instruction with ont une portée limitée à l'intérieur de l'instruction with. Voyons l'exemple suivant

{{ $name := "mike" }}
{{ with $name := "jack"  }}
  {{- $name -}}
{{ end }}
{{- $name -}}

Sa sortie est la suivante, il est clair que ce sont deux variables différentes en raison de portées différentes.

jackmike

Avec l'instruction with, on peut aussi réécrire l'objet racine dans la portée, comme suit

{{ with .name }}
  name: {{- .second }}-{{ .first -}}
{{ end }}
age: {{ .age }}
address: {{ .address }}

Données passées

go
map[string]any{
    "name": map[string]any{
        "first":  "jack",
        "second": "bob",
    },
    "age":     1,
    "address": "usa",
}

Sa sortie

name:bob-jack
age: 1
address: usa

On voit que dans l'instruction with, l'objet racine . est devenu .name.

Conditions

Le format de l'instruction conditionnelle est le suivant

{{ if pipeline }}
  text1
{{ else if pipeline }}
  text2
{{ else }}
  text3
{{ end }}

C'est comme écrire du code ordinaire, très facile à comprendre. Voyons quelques exemples simples,

{{ if eq .lang "en" }}
  {{- .content.en -}}
{{ else if eq .lang "zh" }}
  {{- .content.zh -}}
{{ else }}
  {{- .content.fallback -}}
{{ end }}

Données passées

go
map[string]any{
    "lang": "zh",
    "content": map[string]any{
        "en":       "hello, world!",
        "zh":       "bonjour, monde !",
        "fallback": "hello, world!",
    },
}

Dans l'exemple, le template décide du contenu à afficher en fonction de la langue lang passée. Résultat

bonjour, monde !

Itération

Le format de l'instruction d'itération est le suivant. Le pipeline supporté par range doit être un tableau, une tranche, une map, ou un channel.

{{ range pipeline }}
  loop body
{{ end }}

Avec else, quand la longueur est 0, le contenu du bloc else est exécuté.

{{ range pipeline }}
  loop body
{{ else }}
  fallback
{{ end }}

De plus, on supporte break, continue et ce genre d'opérations, par exemple

{{ range pipeline }}
  {{ if pipeline }}
    {{ break }}
  {{ end }}
  {{ if pipeline }}
    {{ continue }}
  {{ end }}
  loop body
{{ end }}

Voyons un exemple d'itération.

{{ range $index, $val := . }}
  {{- if eq $index 0 }}
    {{- continue -}}
  {{ end -}}
  {{- $index}}: {{ $val }}
{{ end }}

Données passées

go
[]any{1, "2", 3.14},

Sortie

1: 2
2: 3.14

L'itération sur les map fonctionne de la même manière.

Imbrication

Un template peut contenir plusieurs templates définis, par exemple

{{ define "t1" }} t1 {{ end }}
{{ define "t2" }} t2 {{ end }}

Ces templates définis ne seront pas générés dans le template final, sauf si le nom est spécifié au chargement ou s'il est manuellement spécifié par l'instruction template.

func (t *Template) ExecuteTemplate(wr io.Writer, name string, data any) error

Par exemple, avec le code suivant

{{ define "t1" }}
    {{- with .t1 }}
      {{- .data -}}
    {{ end -}}
{{ end }}
{{ define "t2" }}
    {{- with .t2 }}
      {{- .data -}}
    {{ end}}
{{ end -}}

Données passées

go
map[string]any{
    "t1": map[string]any{"data": "template body 1"},
    "t2": map[string]any{"data": "template body 2"},
}

Code

go
func main() {
  out := os.Stdout

  tmpl :=
    `{{ define "t1" }}
    {{- with .t1 }}
      {{- .data -}}
    {{ end -}}
{{ end }}
{{ define "t2" }}
    {{- with .t2 }}
      {{- .data -}}
    {{ end}}
{{ end -}}`

  datas := []any{
    map[string]any{
      "t1": map[string]any{"data": "template body 1"},
      "t2": map[string]any{"data": "template body 2"},
    },
  }

  name := "t1"

  for _, data := range datas {
    err := ExecTmpl(out, tmpl, name, data)
    if err != nil {
      panic(err)
    }
  }
}

func ExecTmpl(writer io.Writer, tmpl string, name string, data any) error {
  t := template.New("template")
  parsedTmpl, err := t.Parse(tmpl)
  if err != nil {
    return err
  }
  return parsedTmpl.ExecuteTemplate(writer, name, data)
}

Sortie

template body 1

Ou on peut aussi spécifier manuellement le template

{{ define "t1" }}
    {{- with .t1 }}
      {{- .data -}}
    {{ end -}}
{{ end }}
{{ define "t2" }}
    {{- with .t2 }}
      {{- .data -}}
    {{ end}}
{{ end -}}
{{ template "t2" .}}

Alors lors de l'analyse, que le nom du template soit spécifié ou non, t2 sera chargé.

Association

Un sous-template n'est qu'une déclaration de plusieurs templates nommés à l'intérieur d'un template. L'association consiste à relier plusieurs *Template nommés externes. Ensuite, on référence le template spécifié par l'instruction template.

{{ tempalte "templateName" pipeline}}

pipeline peut spécifier l'objet racine du template associé selon vos besoins, ou passer directement l'objet racine du template actuel. Voyons l'exemple de code suivant

go
func main() {
  tmpl1 := `name: {{ .name }}`

  tmpl2 := `age: {{ .age }}`

  tmpl3 := `Person Info
{{template "t1" .}}
{{template "t2" .}}`

  t1, err := template.New("t1").Parse(tmpl1)
  if err != nil {
    panic(err)
  }

  t2, err := template.New("t2").Parse(tmpl2)
  if err != nil {
    panic(err)
  }

  t3, err := template.New("t3").Parse(tmpl3)
  if err != nil {
    panic(err)
  }

  if err := associate(t3, t1, t2); err != nil {
    panic(err)
  }

  err = t3.Execute(os.Stdout, map[string]any{
    "name": "jack",
    "age":  18,
  })
  if err != nil {
    panic(err)
  }
}

func associate(t *template.Template, ts ...*template.Template) error {
  for _, tt := range ts {
    _, err := t.AddParseTree(tt.Name(), tt.Tree)
    if err != nil {
      return err
    }
  }
  return nil
}

Dans le code ci-dessus, t3 est associé à t1 et t2, en utilisant la méthode *Template.AddParseTree

go
func (t *Template) AddParseTree(name string, tree *parse.Tree) (*Template, error)

Le résultat final du template est

Person Info
name: jack
age: 18

Slots

Avec l'instruction block, on peut réaliser un effet similaire aux slots de Vue, dans le but de réutiliser un template. Voyons un exemple d'utilisation. Dans le template t1, on définit un slot

Basic Person Info
name: {{ .name }}
age: {{ .age }}
address: {{ .address }}
{{ block "slot" . }} default content body {{ end }}

L'instruction block peut avoir un contenu par défaut dans le slot. Lorsque d'autres templates utilisent le slot plus tard, le contenu par défaut sera remplacé. Dans le template t2, on référence le template t1 et on utilise define pour définir le contenu à intégrer

{{ template "person.txt" . }}
{{ define "slot" }}
school: {{ .school }}
{{ end }}

Après avoir associé les deux templates, on passe les données suivantes

go
map[string]any{
    "name":    "jack",
    "age":     18,
    "address": "usa",
    "company": "google",
    "school":  "mit",
}

Le résultat final est

Basic Person Info
name: jack
age: 18
address: usa

school: mit

Fichiers de template

Dans les exemples de syntaxe de template, nous avons utilisé des littéraux de chaîne comme templates. En pratique, la plupart du temps les templates sont placés dans des fichiers.

go
func ParseFS(fsys fs.FS, patterns ...string) (*Template, error)

Par exemple, template.ParseFs charge les templates correspondant au pattern depuis le système de fichiers spécifié. L'exemple suivant utilise embed.FS comme système de fichiers, avec trois fichiers préparés

txt
# person.txt
Basic Person Info
name: {{ .name }}
age: {{ .age }}
address: {{ .address }}
{{ block "slot" . }} {{ end }}

# student.txt
{{ template "person.txt" . }}
{{ define "slot" }}
school: {{ .school }}
{{ end }}

# employee.txt
{{ template "person.txt" . }}
{{ define "slot" }}
company: {{ .company }}
{{ end }}

Code

go
import (
  "embed"
  "os"
  "text/template"
)

//go:embed *.txt
var fs embed.FS

func main() {
  data := map[string]any{
    "name":    "jack",
    "age":     18,
    "address": "usa",
    "company": "google",
    "school":  "mit",
  }

  t1, err := template.ParseFS(fs, "person.txt", "student.txt")
  if err != nil {
    panic(err)
  }

  t1.Execute(os.Stdout, data)

  t2, err := template.ParseFS(fs, "person.txt", "employee.txt")
  if err != nil {
    panic(err)
  }
  t2.Execute(os.Stdout, data)
}

Sortie

Basic Person Info
name: jack
age: 18
address: usa

school: mit
Basic Person Info
name: jack
age: 18
address: usa

company: google

C'est un exemple très simple d'utilisation de fichiers de template. person.txt sert de fichier de slot, les deux autres réutilisent son contenu et intègrent un nouveau contenu personnalisé. On peut aussi utiliser les deux fonctions suivantes

go
func ParseGlob(pattern string) (*Template, error)

func ParseFiles(filenames ...string) (*Template, error)

ParseGlob est basé sur la correspondance de caractères génériques, ParseFiles est basé sur les noms de fichiers. Elles utilisent toutes deux le système de fichiers local. Si vous utilisez des fichiers html destinés à être affichés sur le frontend, il est recommandé d'utiliser le package html/template, qui fournit une API identique à text/template, mais avec des traitements de sécurité pour html, css, js.

Golang by www.golangdev.cn edit