Skip to content

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:

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)
  }
}

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:

go
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.

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

Bei der zukünftigen Verwendung von Template-Engines sind es im Wesentlichen immer diese drei Schritte:

  1. Template laden
  2. Template parsen
  3. 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:

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)
}

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:

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)
    }
  }
}

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:

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)
    }
  }
}

Ausgabe:

data-> second
data-> first

Bei 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:

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)
    }
  }
}

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>2

Zu 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:

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)
    }
  }
}

Ausgabe:

jack

Variablen 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:

go
type FuncMap map[string]any

Beim Erstellen eines Templates wird die Standard-Funktionszuordnungstabelle aus text/template.builtins abgerufen. Im Folgenden sind alle eingebauten Funktionen aufgeführt:

FunktionsnameBedeutungBeispiel
andUND-Verknüpfung{{ and true false }}
orODER-Verknüpfung{{ or true false }}
notNegation{{ not true }}
eqGleichheit{{ eq 1 2 }}
neUngleichheit{{ ne 1 2 }}
ltKleiner als{{ lt 1 2 }}
leKleiner oder gleich{{ le 1 2 }}
gtGrößer als{{ gt 1 2 }}
geGrößer oder gleich{{ ge 1 2 }}
lenGibt Länge zurück{{ len .slice }}
indexElement am angegebenen Index{{ index . 0 }}
sliceSlice, entspricht s[1:2:3]{{ slice . 1 2 3 }}
htmlHTML-Escaping{{ html .name }}
jsJS-Escaping{{ js .name }}
printfmt.Sprint{{ print . }}
printffmt.Sprintf{{ printf "%s" .}}
printlnfmt.Sprintln{{ println . }}
urlqueryURL-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:

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

Im Template wird dann generiert:

1024: 2048

Dies 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.

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

Benutzerdefinierte Funktionen haben normalerweise zwei Rückgabewerte: Der erste ist der zu verwendende Rückgabewert, der zweite ist error. Zum Beispiel:

go
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 + 1

Pipeline

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:

jackmike

Mit der with-Anweisung kann das Wurzelobjekt innerhalb des Gültigkeitsbereichs überschrieben werden:

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

Übergebene Daten:

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

Die Ausgabe:

name:bob-jack
age: 1
address: usa

Man 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:

go
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:

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

Ausgabe:

1: 2
2: 3.14

Die 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.

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

Ein Beispiel:

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

Übergebene Daten:

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)
}

Ausgabe:

template body 1

Oder 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:

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
}

Im obigen Code ist t3 mit t1 und t2 verknüpft. Die Methode *Template.AddParseTree wird für die Verknüpfung verwendet:

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

Das endgültige Template-Ergebnis ist:

Person Info
name: jack
age: 18

Slots

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:

go
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: mit

Template-Dateien

In den Beispielen zur Template-Syntax wurden String-Literale als Templates verwendet. In der Praxis werden Templates meistens in Dateien abgelegt.

go
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:

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 }}

Der Code lautet wie folgt:

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)
}

Die Ausgabe ist:

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

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

company: google

Dies 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:

go
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.

Golang by www.golangdev.cn edit