template
Offizielle Dokumentation: template package - text/template - Go Packages
Im Alltag verwenden wir häufig die Funktion fmt.Sprintf zur String-Formatierung. Diese eignet sich jedoch nur für kleine Strings und erfordert Formatierungsverben zur Typangabe. Parameter können nicht benannt werden und komplexe Fälle werden nicht unterstützt. Genau das ist das Problem, das Template-Engines lösen. Zum Beispiel werden statische HTML-Seiten, die direkt am Backend hängen, mit Template-Engines benötigt. In der Community gibt es viele exzellente Drittanbieter-Template-Engines wie pongo2, sprig, jet. In diesem Artikel geht es jedoch um die eingebaute Template-Engine text/template. In der praktischen Entwicklung wird normalerweise html/template verwendet, das auf Ersterem basiert und viele Sicherheitsfunktionen für HTML bietet. Für allgemeine Zwecke reicht Ersteres, bei HTML-Template-Verarbeitung sollte Letzteres verwendet werden.
Schnellstart
Schauen wir uns ein einfaches Beispiel für die Verwendung einer Template-Engine an:
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)
}
}Die Ausgabe des obigen Codes ist:
This is the first template string, hello world!Im Beispiel ist tmpl ein Template-String. {{ .message }} im String ist ein Template-Parameter. Zuerst wird der Template-String mit der Methode *Template.Parse geparst:
func (t *Template) Parse(text string) (*Template, error)Nach erfolgreichem Parsen werden die data mit der Methode *Template.Execute auf das Template angewendet und an den übergebenen Writer (hier os.Stdout) ausgegeben.
func (t *Template) Execute(wr io.Writer, data any) errorBei der zukünftigen Verwendung von Template-Engines sind es im Wesentlichen immer diese drei Schritte:
- Template laden
- Template parsen
- Daten auf das Template anwenden
Die Verwendung einer Template-Engine ist also recht einfach. Etwas komplexer ist die Template-Syntax, die der Hauptinhalt dieses Artikels ist.
Template-Syntax
Parameter
Go verwendet zwei geschweifte Klammern {{ }}, um einen Template-Parameter im Template zu kennzeichnen. Mit . wird das Wurzelobjekt angesprochen, das ist das übergebene data. Wie beim Zugriff auf Membervariablen eines Typs kann man mit dem .-Symbol den Variablennamen verbinden, um auf den entsprechenden Wert im Template zuzugreifen, zum Beispiel:
{{ .data }}Voraussetzung ist, dass eine Membervariable mit demselben Namen existiert, sonst tritt ein Fehler auf. Für das übergebene data handelt es sich normalerweise um eine Struktur oder map, es können aber auch Basistypen wie Zahlen oder Strings sein. In diesem Fall repräsentiert . das Wurzelobjekt selbst. In den geschweiften Klammern muss nicht unbedingt auf das Wurzelobjekt zugegriffen werden, es können auch Literale von Basistypen sein, zum Beispiel:
{{ 1 }}
{{ 3.14 }}
{{ "jack" }}Unabhängig vom Typ wird schließlich durch fmt.Sprintf("%s", val) die String-Repräsentation ermittelt. Sehen wir uns das folgende Beispiel an:
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)
}Ausgabe:
data-> hello world!
data-> 6379
data-> 3.1415926
data-> [1 2*2 3.6]
data-> map[data:hello world!]
data-> {hello world!}Man sieht, dass die Ausgabeform mit der direkten Verwendung von fmt.Sprintf übereinstimmt. Für Strukturen und Maps kann über den Feldnamen auf deren Werte zugegriffen werden:
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)
}
}
}Ausgabe:
data-> hello world!
data-> hello world!Für Slices und map gibt es zwar keine spezielle Syntax für den Zugriff auf einen bestimmten Index, aber dies kann durch Funktionsaufrufe erreicht werden:
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)
}
}
}Ausgabe:
data-> second
data-> firstBei mehrdimensionalen Slices kann wie folgt auf den Wert am entsprechenden Index zugegriffen werden, was s[i][j][k] entspricht:
{{ index . i j k }}Bei verschachtelten Strukturen oder Maps kann mit .k1.k2.k3 zugegriffen werden, zum Beispiel:
{{ .person.father.name }}Bei der Verwendung von Template-Parametern kann das --Symbol vor und nach dem Parameter hinzugefügt werden, um Leerzeichen vor und nach dem Parameter zu entfernen. Ein Beispiel:
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)
}
}
}Normalerweise sollte die Ausgabe 10 > 2 sein, aber da das --Symbol vor und nach dem op-Parameter hinzugefügt wurde, werden die Leerzeichen davor und dahinter entfernt. Die tatsächliche Ausgabe ist:
10>2Zu beachten ist, dass in den geschweiften Klammern das --Symbol und der Parameter durch ein Leerzeichen getrennt sein müssen. Das Format muss also {{- . -}} sein. Im Beispiel wurden zusätzlich Leerzeichen eingefügt und als {{ - . - }} geschrieben, was rein optischen Gründen dient. Tatsächlich gibt es diese Syntax-Einschränkung nicht.
Kommentare
Die Template-Syntax unterstützt Kommentare. Kommentare werden im endgültigen Template nicht generiert. Die Syntax lautet:
{{/* this is a comment */}}Die Kommentarsymbole /* und */ müssen an die geschweiften Klammern angrenzen. Zwischen ihnen dürfen keine anderen Zeichen stehen, sonst kann das Template nicht korrekt geparst werden. Es gibt nur eine Ausnahme: beim Entfernen von Leerzeichen:
{{- /* this is a comment */ -}}Variablen
Im Template können auch Variablen deklariert werden. Mit dem $-Symbol wird gekennzeichnet, dass es sich um eine Variable handelt, und mit := wird zugewiesen, genau wie in Go-Code. Ein Beispiel:
{{ $name := .Name }}
{{ $val := index . 1 }}
{{ $val := index .dict key }}
// Integer-Zuweisung
{{ $numer := 1 }}
// Fließkomma-Zuweisung
{{ $float := 1.234}}
// String-Zuweisung
{{ $name := "jack" }}Bei der späteren Verwendung wird mit $ und dem Variablennamen auf den Wert der Variable zugegriffen, zum Beispiel:
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)
}
}
}Ausgabe:
jackVariablen müssen vor der Verwendung deklariert werden, sonst wird undefined variable angezeigt. Außerdem müssen sie innerhalb des Gültigkeitsbereichs verwendet werden.
Funktionen
Die Template-Syntax selbst ist nicht sehr umfangreich. Die meisten Funktionen werden durch Funktionen implementiert. Das Format für Funktionsaufrufe ist: Funktionsname gefolgt von der Parameterliste, getrennt durch Leerzeichen:
{{ funcname arg1 arg2 arg3 ... }}Zum Beispiel die zuvor verwendete index-Funktion:
{{ index .s 1 }}Die eq-Funktion zum Vergleich auf Gleichheit:
{{ eq 1 2 }}Jedes *Template hat eine FuncsMap für die Zuordnung von Funktionen:
type FuncMap map[string]anyBeim Erstellen eines Templates wird die Standard-Funktionszuordnungstabelle aus text/template.builtins abgerufen. Im Folgenden sind alle eingebauten Funktionen aufgeführt:
| Funktionsname | Bedeutung | Beispiel |
|---|---|---|
and | UND-Verknüpfung | {{ and true false }} |
or | ODER-Verknüpfung | {{ or true false }} |
not | Negation | {{ not true }} |
eq | Gleichheit | {{ eq 1 2 }} |
ne | Ungleichheit | {{ ne 1 2 }} |
lt | Kleiner als | {{ lt 1 2 }} |
le | Kleiner oder gleich | {{ le 1 2 }} |
gt | Größer als | {{ gt 1 2 }} |
ge | Größer oder gleich | {{ ge 1 2 }} |
len | Gibt Länge zurück | {{ len .slice }} |
index | Element am angegebenen Index | {{ index . 0 }} |
slice | Slice, entspricht s[1:2:3] | {{ slice . 1 2 3 }} |
html | HTML-Escaping | {{ html .name }} |
js | JS-Escaping | {{ js .name }} |
print | fmt.Sprint | {{ print . }} |
printf | fmt.Sprintf | {{ printf "%s" .}} |
println | fmt.Sprintln | {{ println . }} |
urlquery | URL-Query-Escaping | {{ urlquery .query }} |
Zusätzlich gibt es eine spezielle eingebaute Funktion call. Sie wird verwendet, um Funktionen aufzurufen, die zur Laufzeit über data übergeben werden. Zum Beispiel das folgende Template:
{{ call .string 1024 }}Übergebene Daten:
map[string]any{
"string": func(val any) string { return fmt.Sprintf("%v: 2048", val) },
}Im Template wird dann generiert:
1024: 2048Dies ist eine Möglichkeit, benutzerdefinierte Funktionen hinzuzufügen. Normalerweise wird jedoch empfohlen, die Methode *Template.Funcs zu verwenden, um benutzerdefinierte Funktionen hinzuzufügen, da diese global verfügbar sind und nicht an das Wurzelobjekt gebunden werden müssen.
func (t *Template) Funcs(funcMap FuncMap) *TemplateBenutzerdefinierte Funktionen haben normalerweise zwei Rückgabewerte: Der erste ist der zu verwendende Rückgabewert, der zweite ist error. Zum Beispiel:
template.FuncMap{
"add": func(val any) (string, error) { return fmt.Sprintf("%v+1", val), nil },
}Dann kann sie direkt im Template verwendet werden:
{{ add 1024 }}Das Ergebnis ist:
1024 + 1Pipeline
Diese Pipeline ist etwas anderes als chan. In der offiziellen Dokumentation wird sie als pipeline bezeichnet. Jede Operation, die Daten produzieren kann, wird als pipeline bezeichnet. Die folgenden Template-Operationen sind alle Pipeline-Operationen:
{{ 1 }}
{{ eq 1 2 }}
{{ $name }}
{{ .name }}Wer Linux kennt, weiß, dass es den Pipe-Operator | gibt. Templates unterstützen ebenfalls diese Schreibweise. Pipeline-Operationen kommen in Templates häufig vor, zum Beispiel:
{{ $name := 1 }}{{ $name | print | printf "%s+1=?" }}Das Ergebnis ist:
1+1=?In den folgenden with, if, range wird dies ebenfalls häufig verwendet.
with
Mit der with-Anweisung können der Gültigkeitsbereich von Variablen und das Wurzelobjekt gesteuert werden. Das Format lautet:
{{ with pipeline }}
text
{{ end }}with prüft den von der Pipeline-Operation zurückgegebenen Wert. Wenn der Wert leer ist, wird der zwischengeschaltete text nicht generiert. Wenn der leere Fall behandelt werden soll, kann with else verwendet werden:
{{ with pipeline }}
text1
{{ else }}
text2
{{ end }}Wenn die Pipeline-Operation einen leeren Wert zurückgibt, wird der else-Block ausgeführt. Variablen, die innerhalb der with-Anweisung deklariert werden, sind nur innerhalb der with-Anweisung gültig. Ein Beispiel:
{{ $name := "mike" }}
{{ with $name := "jack" }}
{{- $name -}}
{{ end }}
{{- $name -}}Die Ausgabe lautet wie folgt. Offensichtlich sind es aufgrund unterschiedlicher Gültigkeitsbereiche zwei verschiedene Variablen:
jackmikeMit der with-Anweisung kann das Wurzelobjekt innerhalb des Gültigkeitsbereichs überschrieben werden:
{{ with .name }}
name: {{- .second }}-{{ .first -}}
{{ end }}
age: {{ .age }}
address: {{ .address }}Übergebene Daten:
map[string]any{
"name": map[string]any{
"first": "jack",
"second": "bob",
},
"age": 1,
"address": "usa",
}Die Ausgabe:
name:bob-jack
age: 1
address: usaMan sieht, dass innerhalb der with-Anweisung das Wurzelobjekt . zu .name geworden ist.
Bedingung
Das Format der Bedingungsanweisung lautet:
{{ if pipeline }}
text1
{{ else if pipeline }}
text2
{{ else }}
text3
{{ end }}Es ist wie das Schreiben von normalem Code, sehr verständlich. Hier sind einige einfache Beispiele:
{{ if eq .lang "en" }}
{{- .content.en -}}
{{ else if eq .lang "zh" }}
{{- .content.zh -}}
{{ else }}
{{- .content.fallback -}}
{{ end }}Übergebene Daten:
map[string]any{
"lang": "zh",
"content": map[string]any{
"en": "hello, world!",
"zh": "Hallo, Welt!",
"fallback": "hello, world!",
},
}Das Template im Beispiel entscheidet basierend auf der übergebenen Sprache lang, wie der Inhalt angezeigt wird. Ausgabe:
Hallo, Welt!Iteration
Das Format der Iterationsanweisung lautet wie folgt. Die von range unterstützte pipeline muss ein Array, Slice, map oder channel sein.
{{ range pipeline }}
loop body
{{ end }}In Kombination mit else wird der Inhalt des else-Blocks ausgeführt, wenn die Länge 0 ist:
{{ range pipeline }}
loop body
{{ else }}
fallback
{{ end }}Zusätzlich werden Operationen wie break und continue unterstützt:
{{ range pipeline }}
{{ if pipeline }}
{{ break }}
{{ end }}
{{ if pipeline }}
{{ continue }}
{{ end }}
loop body
{{ end }}Hier ist ein Beispiel für eine Iteration:
{{ range $index, $val := . }}
{{- if eq $index 0 }}
{{- continue -}}
{{ end -}}
{{- $index}}: {{ $val }}
{{ end }}Übergebene Daten:
[]any{1, "2", 3.14},Ausgabe:
1: 2
2: 3.14Die Iteration über map funktioniert analog.
Verschachtelung
In einem Template können mehrere Templates definiert werden:
{{ define "t1" }} t1 {{ end }}
{{ define "t2" }} t2 {{ end }}Diese definierten Templates werden nicht im endgültigen Template generiert, es sei denn, beim Laden wird ein Name angegeben oder sie werden manuell über die template-Anweisung angegeben.
func (t *Template) ExecuteTemplate(wr io.Writer, name string, data any) errorEin Beispiel:
{{ define "t1" }}
{{- with .t1 }}
{{- .data -}}
{{ end -}}
{{ end }}
{{ define "t2" }}
{{- with .t2 }}
{{- .data -}}
{{ end}}
{{ end -}}Übergebene Daten:
map[string]any{
"t1": map[string]any{"data": "template body 1"},
"t2": map[string]any{"data": "template body 2"},
}Code:
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)
}Ausgabe:
template body 1Oder das Template kann manuell angegeben werden:
{{ define "t1" }}
{{- with .t1 }}
{{- .data -}}
{{ end -}}
{{ end }}
{{ define "t2" }}
{{- with .t2 }}
{{- .data -}}
{{ end}}
{{ end -}}
{{ template "t2" .}}Dann wird t2 beim Parsen geladen, unabhängig davon, ob ein Template-Name angegeben wurde.
Verknüpfung
Unter-Templates sind nur mehrere benannte Templates, die innerhalb eines Templates deklariert werden. Verknüpfung ist das Verbinden mehrerer externer benannter *Template. Dann wird mit der template-Anweisung auf das angegebene Template verwiesen:
{{ template "templateName" pipeline}}pipeline kann je nach Bedarf das Wurzelobjekt für das verknüpfte Template angeben, oder das aktuelle Wurzelobjekt übergeben. Ein Codebeispiel:
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
}Im obigen Code ist t3 mit t1 und t2 verknüpft. Die Methode *Template.AddParseTree wird für die Verknüpfung verwendet:
func (t *Template) AddParseTree(name string, tree *parse.Tree) (*Template, error)Das endgültige Template-Ergebnis ist:
Person Info
name: jack
age: 18Slots
Mit der block-Anweisung kann ein ähnlicher Effekt wie bei Vue-Slots erzielt werden. Der Zweck ist die Wiederverwendung eines Templates. Anhand eines Beispiels sieht man, wie es funktioniert. Im t1-Template wird ein Slot definiert:
Basic Person Info
name: {{ .name }}
age: {{ .age }}
address: {{ .address }}
{{ block "slot" . }} default content body {{ end }}Die block-Anweisung kann Standard-Inhalte im Slot enthalten. Wenn andere Templates den Slot verwenden, wird der Standard-Inhalt überschrieben. Im t2-Template wird auf t1 verwiesen und mit define wird der einzubettende Inhalt definiert:
{{ template "person.txt" . }}
{{ define "slot" }}
school: {{ .school }}
{{ end }}Nachdem die beiden Templates verknüpft wurden, werden folgende Daten übergeben:
map[string]any{
"name": "jack",
"age": 18,
"address": "usa",
"company": "google",
"school": "mit",
}Die endgültige Ausgabe ist:
Basic Person Info
name: jack
age: 18
address: usa
school: mitTemplate-Dateien
In den Beispielen zur Template-Syntax wurden String-Literale als Templates verwendet. In der Praxis werden Templates meistens in Dateien abgelegt.
func ParseFS(fsys fs.FS, patterns ...string) (*Template, error)Zum Beispiel lädt template.ParseFS Templates aus dem angegebenen Dateisystem, die dem pattern entsprechen. Das folgende Beispiel verwendet embed.FS als Dateisystem. Drei Dateien werden vorbereitet:
# 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 }}Der Code lautet wie folgt:
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)
}Die Ausgabe ist:
Basic Person Info
name: jack
age: 18
address: usa
school: mit
Basic Person Info
name: jack
age: 18
address: usa
company: googleDies ist ein sehr einfaches Beispiel für die Verwendung von Template-Dateien. person.txt dient als Slot-Datei, die anderen beiden wiederverwenden ihren Inhalt und betten benutzerdefinierte neue Inhalte ein. Alternativ können die folgenden zwei Funktionen verwendet werden:
func ParseGlob(pattern string) (*Template, error)
func ParseFiles(filenames ...string) (*Template, error)ParseGlob basiert auf Wildcard-Matching, ParseFiles auf Dateinamen. Beide verwenden das lokale Dateisystem. Bei html-Dateien, die im Frontend angezeigt werden, wird empfohlen, das Paket html/template zu verwenden. Es bietet dieselbe API wie text/template, enthält aber Sicherheitsfunktionen für html, css und js.
