template
Documentazione ufficiale: template package - text/template - Go Packages
Spesso utilizziamo la funzione fmt.Sprintf per formattare le stringhe, ma è adatta solo per gestire piccole stringhe e richiede l'uso di verbi di formattazione per specificare i tipi, non supporta la denominazione dei parametri e non gestisce situazioni complesse. Questo è il problema che il motore di template deve risolvere, ad esempio le pagine HTML statiche collegate al backend richiedono l'uso di un motore di template. Nella community ci sono molte eccellenti librerie di motori di template di terze parti come pongo2, sprig, jet, ma il protagonista di questo articolo è il motore di template integrato di Go text/template. Nello sviluppo effettivo si usa generalmente html/template, che si basa sul primo e ha apportato molte elaborazioni di sicurezza relative all'HTML. In generale si può usare il primo, ma se si tratta di elaborazione di template HTML si consiglia di usare il secondo per maggiore sicurezza.
Inizio Rapido
Di seguito un semplice esempio di utilizzo del motore di template, come mostrato
package main
import (
"fmt"
"os"
"text/template"
)
func main() {
tmpl := `Questo è il primo template stringa, {{ .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)
}
}L'output del codice sopra è
Questo è il primo template stringa, hello world!Nel codice del caso, tmpl è una stringa template, {{ .message }} nella stringa è un parametro template del motore di template. Per prima cosa si analizza la stringa template tramite il metodo *Template.Parse,
func (t *Template) Parse(text string) (*Template, error)Dopo l'analisi riuscita, si applicano i dati data al template tramite il metodo *Template.Execute, infine si outputa al Writer passato, cioè os.Stdout.
func (t *Template) Execute(wr io.Writer, data any) errorNell'uso futuro del motore di template, ci sono fondamentalmente questi tre passaggi:
- Ottenere il template
- Analizzare il template
- Applicare i dati al template
Come si può vedere, l'uso del motore di template è piuttosto semplice. Ciò che è leggermente più complesso è la sintassi del template del motore di template, che è il contenuto principale spiegato in questo articolo.
Sintassi del Template
Parametri
Go utilizza due coppie di parentesi graffe {{ }} per indicare un parametro template nella stringa, e utilizza . per rappresentare l'oggetto root. L'oggetto root è il data passato. Come accedere alle variabili membro di un tipo, si utilizza il simbolo . per collegare il nome della variabile per accedere al valore corrispondente nel template, ad esempio
{{ .data }}A condizione che esista una variabile membro con lo stesso nome, altrimenti verrà generato un errore. Per il data passato, generalmente è una struct o una map, ma può anche essere un tipo di base, come numeri o stringhe. In questo caso, . rappresenta l'oggetto stesso. All'interno delle parentesi graffe, non è necessario accedere all'oggetto root per ottenere il valore, ma può anche essere un letterale di tipo di base, ad esempio
{{ 1 }}
{{ 3.14 }}
{{ "jack" }}Indipendentemente dal tipo, alla fine verrà ottenuta la sua rappresentazione come stringa tramite fmt.Sprintf("%s", val). Guarda l'esempio seguente.
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)
}L'output è il seguente
data-> hello world!
data-> 6379
data-> 3.1415926
data-> [1 2*2 3.6]
data-> map[data:hello world!]
data-> {hello world!}Come si può vedere, l'output è coerente con l'uso diretto di fmt.Sprintf. Per struct e map, è possibile accedere ai valori tramite il nome del campo, come mostrato di seguito
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)
}
}
}L'output è il seguente
data-> hello world!
data-> hello world!Per slice e map, sebbene non fornisca una sintassi specifica per accedere a un valore di indice specifico, è possibile implementarlo tramite chiamate di funzione, come mostrato di seguito
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)
}
}
}Output
data-> second
data-> firstSe si tratta di una slice multidimensionale, è possibile accedere al valore dell'indice corrispondente nel modo seguente, equivalente a s[i][j][k]
{{ index . i j k }}Per struct o map nidificate, è possibile utilizzare .k1.k2.k3 per accedere, ad esempio
{{ .person.father.name }}Quando si utilizzano i parametri del template, è possibile aggiungere il simbolo - prima e dopo il parametro per eliminare gli spazi vuoti prima e dopo il parametro. Guarda un esempio
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)
}
}
}Normalmente l'output dovrebbe essere 10 > 2, ma poiché sono stati aggiunti i simboli - prima e dopo il parametro op, gli spazi vuoti prima e dopo verranno eliminati, quindi l'output effettivo è
10>2Va notato che nelle parentesi graffe, il simbolo - deve essere separato dal parametro da uno spazio, cioè deve essere nel formato {{- . -}}. Nell'esempio, è stato scritto nel formato {{ - . - }} con spazi aggiuntivi su entrambi i lati puramente per estetica personale, in realtà non c'è questo limite di sintassi.
Commenti
La sintassi del template supporta i commenti, i commenti non verranno generati nel template finale, la sintassi è la seguente
{{/* questo è un commento */}}I simboli di commento /* e */ devono essere adiacenti alle parentesi graffe, non ci possono essere altri caratteri tra di loro, altrimenti non potrà essere analizzato correttamente. C'è solo un'eccezione, ovvero quando si eliminano gli spazi vuoti
{{- /* questo è un commento */ -}}Variabili
È possibile dichiarare variabili anche nel template, utilizzando il simbolo $ per indicare che si tratta di una variabile, e assegnando tramite := , proprio come nel codice Go. Esempio:
{{ $name := .Name }}
{{ $val := index . 1 }}
{{ $val := index .dict key }}
// Assegnazione intero
{{ $numer := 1 }}
// Assegnazione virgola mobile
{{ $float := 1.234}}
// Assegnazione stringa
{{ $name := "jack" }}Nell'uso successivo, si accede al valore della variabile tramite $ seguito dal nome della variabile, ad esempio
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)
}
}
}Output
jackLe variabili devono essere dichiarate prima dell'uso, altrimenti verrà visualizzato undefined variable, e devono essere utilizzate anche all'interno del loro scope.
Funzioni
La sintassi del template di per sé non è molto complessa, la maggior parte delle funzionalità sono implementate tramite funzioni. Il formato della chiamata di funzione è il nome della funzione seguito dall'elenco dei parametri, separati da spazi, come mostrato di seguito
{{ funcname arg1 arg2 arg3 ... }}Ad esempio, la funzione index utilizzata in precedenza
{{ index .s 1 }}La funzione eq utilizzata per confrontare se sono uguali
{{ eq 1 2 }}Ogni *Template ha una FuncsMap, utilizzata per registrare le mappature delle funzioni
type FuncMap map[string]anyQuando si crea un template, si ottiene la tabella di mappatura delle funzioni predefinita da text/template.builtins. Di seguito sono riportate tutte le funzioni integrate
| Nome Funzione | Scopo | Esempio |
|---|---|---|
and | Operazione AND | {{ and true false }} |
or | Operazione OR | {{ or true false }} |
not | Operazione NOT | {{ not true }} |
eq | Se sono uguali | {{ eq 1 2 }} |
ne | Se sono diversi | {{ ne 1 2 }} |
lt | Minore di | {{ lt 1 2 }} |
le | Minore o uguale | {{ le 1 2 }} |
gt | Maggiore di | {{ gt 1 2 }} |
ge | Maggiore o uguale | {{ ge 1 2 }} |
len | Restituisce la lunghezza | {{ len .slice }} |
index | Ottiene l'elemento all'indice specificato | {{ index . 0 }} |
slice | Slice, equivalente a s[1:2:3] | {{ slice . 1 2 3 }} |
html | Escape HTML | {{ html .name }} |
js | Escape JS | {{ js .name }} |
print | fmt.Sprint | {{ print . }} |
printf | fmt.Sprintf | {{ printf "%s" .}} |
println | fmt.Sprintln | {{ println . }} |
urlquery | Escape URL query | {{ urlquery .query }} |
Oltre a queste, c'è una funzione integrata speciale call, utilizzata per chiamare direttamente le funzioni passate nei data durante il periodo Execute. Ad esempio, il seguente template
{{ call .string 1024 }}I dati passati sono i seguenti
map[string]any{
"string": func(val any) string { return fmt.Sprintf("%v: 2048", val) },
}Allora nel template verrà generato
1024: 2048Questo è uno dei modi per personalizzare le funzioni, ma di solito si consiglia di utilizzare il metodo *Template.Funcs per aggiungere funzioni personalizzate, poiché quest'ultimo può agire a livello globale senza dover essere legato all'oggetto root.
func (t *Template) Funcs(funcMap FuncMap) *TemplateIl valore di ritorno di una funzione personalizzata generalmente ha due valori, il primo è il valore di ritorno necessario, il secondo è error. Ad esempio, c'è la seguente funzione personalizzata
template.FuncMap{
"add": func(val any) (string, error) { return fmt.Sprintf("%v+1", val), nil },
}Poi si usa direttamente nel template
{{ add 1024 }}Il risultato è
1024 + 1Pipeline
Questa pipeline è diversa da chan, la documentazione ufficiale la chiama pipeline, qualsiasi operazione che può generare dati è chiamata pipeline. Le seguenti operazioni del template appartengono alle operazioni pipeline
{{ 1 }}
{{ eq 1 2 }}
{{ $name }}
{{ .name }}Chi ha familiarità con Linux dovrebbe conoscere l'operatore pipe |, anche il template supporta questa sintassi. Le operazioni pipe appaiono spesso nei template, ad esempio
{{ $name := 1 }}{{ $name | print | printf "%s+1=?" }}Il risultato è
1+1=?Verrà utilizzato frequentemente anche nei successivi with, if, range.
With
Tramite l'istruzione with è possibile controllare lo scope delle variabili e dell'oggetto root. Il formato è il seguente
{{ with pipeline }}
text
{{ end }}with controlla il valore restituito dall'operazione pipeline, se il valore è vuoto, il template text centrale non verrà generato. Se si desidera gestire il caso vuoto, è possibile utilizzare with else. Il formato è il seguente
{{ with pipeline }}
text1
{{ else }}
text2
{{ end }}Se l'operazione pipeline restituisce un valore vuoto, verrà eseguita la logica del blocco else. Le variabili dichiarate nell'istruzione with hanno uno scope limitato all'interno dell'istruzione with. Guarda un esempio
{{ $name := "mike" }}
{{ with $name := "jack" }}
{{- $name -}}
{{ end }}
{{- $name -}}Il suo output è il seguente, ovviamente questo è dovuto a scope diversi, sono due variabili diverse.
jackmikeTramite l'istruzione with è anche possibile riscrivere l'oggetto root all'interno dello scope, come segue
{{ with .name }}
name: {{- .second }}-{{ .first -}}
{{ end }}
age: {{ .age }}
address: {{ .address }}Passando i seguenti dati
map[string]any{
"name": map[string]any{
"first": "jack",
"second": "bob",
},
"age": 1,
"address": "usa",
}Il suo output
name:bob-jack
age: 1
address: usaCome si può vedere, all'interno dell'istruzione with, l'oggetto root . è diventato .name.
Condizioni
Il formato dell'istruzione condizionale è il seguente
{{ if pipeline }}
text1
{{ else if pipeline }}
text2
{{ else }}
text3
{{ end }}Proprio come scrivere codice normale, molto facile da capire. Di seguito alcuni semplici esempi,
{{ if eq .lang "en" }}
{{- .content.en -}}
{{ else if eq .lang "zh" }}
{{- .content.zh -}}
{{ else }}
{{- .content.fallback -}}
{{ end }}I dati passati
map[string]any{
"lang": "zh",
"content": map[string]any{
"en": "hello, world!",
"zh": "你好,世界!",
"fallback": "hello, world!",
},
}Il template nell'esempio decide in base alla lingua lang passata come mostrare il contenuto. Risultato dell'output
你好,世界!Iterazione
Il formato dell'istruzione di iterazione è il seguente, la pipeline supportata da range deve essere array, slice, map, e channel.
{{ range pipeline }}
loop body
{{ end }}Combinando con else, quando la lunghezza è 0, verrà eseguito il contenuto del blocco else.
{{ range pipeline }}
loop body
{{ else }}
fallback
{{ end }}Oltre a questo, supporta anche operazioni come break e continue, ad esempio
{{ range pipeline }}
{{ if pipeline }}
{{ break }}
{{ end }}
{{ if pipeline }}
{{ continue }}
{{ end }}
loop body
{{ end }}Di seguito un esempio di iterazione.
{{ range $index, $val := . }}
{{- if eq $index 0 }}
{{- continue -}}
{{ end -}}
{{- $index}}: {{ $val }}
{{ end }}Dati passati
[]any{1, "2", 3.14},Output
1: 2
2: 3.14Iterare su una map è lo stesso principio.
Annidamento
In un template possono essere definiti più template, ad esempio
{{ define "t1" }} t1 {{ end }}
{{ define "t2" }} t2 {{ end }}Questi template definiti non verranno generati nel template finale, a meno che non venga specificato un nome durante il caricamento o specificato manualmente tramite l'istruzione template.
func (t *Template) ExecuteTemplate(wr io.Writer, name string, data any) errorAd esempio, il seguente caso
{{ define "t1" }}
{{- with .t1 }}
{{- .data -}}
{{ end -}}
{{ end }}
{{ define "t2" }}
{{- with .t2 }}
{{- .data -}}
{{ end}}
{{ end -}}Passando i seguenti dati
map[string]any{
"t1": map[string]any{"data": "template body 1"},
"t2": map[string]any{"data": "template body 2"},
}Codice
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)
}Output
template body 1Oppure è possibile specificare manualmente il template
{{ define "t1" }}
{{- with .t1 }}
{{- .data -}}
{{ end -}}
{{ end }}
{{ define "t2" }}
{{- with .t2 }}
{{- .data -}}
{{ end}}
{{ end -}}
{{ template "t2" .}}Allora durante l'analisi, indipendentemente dal fatto che venga specificato il nome del template, t2 verrà caricato.
Associazione
I sottotemplate sono solo template denominati multipli dichiarati all'interno di un template, l'associazione collega più *Template denominati esterni. Quindi si riferisce al template specificato tramite l'istruzione template.
{{ template "templateName" pipeline}}pipeline può specificare l'oggetto root del template associato in base alle proprie esigenze, oppure può passare direttamente l'oggetto root del template corrente. Guarda un esempio di codice
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
}Nel codice sopra, t3 è associato a t1 e t2, utilizzando il metodo *Template.AddParseTree per l'associazione
func (t *Template) AddParseTree(name string, tree *parse.Tree) (*Template, error)Il risultato finale della generazione del template è
Person Info
name: jack
age: 18Slot
Tramite l'istruzione block, è possibile realizzare un effetto simile agli slot di Vue, il cui scopo è riutilizzare un template. Guarda un caso d'uso per capire come funziona. Nel template t1 definisci lo slot
Basic Person Info
name: {{ .name }}
age: {{ .age }}
address: {{ .address }}
{{ block "slot" . }} contenuto predefinito {{ end }}L'istruzione block può avere contenuto predefinito nello slot, quando altri template usano lo slot in seguito, sovrascriveranno il contenuto predefinito. Nel template t2 si riferisce al template t1 e si utilizza define per definire il contenuto da incorporare
{{ template "person.txt" . }}
{{ define "slot" }}
school: {{ .school }}
{{ end }}Dopo aver associato i due template, passando i seguenti dati
map[string]any{
"name": "jack",
"age": 18,
"address": "usa",
"company": "google",
"school": "mit",
}Il risultato finale dell'output è
Basic Person Info
name: jack
age: 18
address: usa
school: mitFile Template
Nei casi di sintassi del template, vengono utilizzati letterali di stringa come template. Nella maggior parte dei casi d'uso effettivi, i template vengono inseriti in file.
func ParseFS(fsys fs.FS, patterns ...string) (*Template, error)Ad esempio, template.ParseFs carica i template che corrispondono al pattern dal filesystem specificato. Il seguente esempio utilizza embed.FS come filesystem, preparando tre file
# 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 }}Il codice è il seguente
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)
}L'output è
Basic Person Info
name: jack
age: 18
address: usa
school: mit
Basic Person Info
name: jack
age: 18
address: usa
company: googleQuesto è un caso d'uso molto semplice di file template, person.txt funge da file slot, gli altri due riutilizzano il suo contenuto e incorporano nuovo contenuto personalizzato. È anche possibile utilizzare le seguenti due funzioni
func ParseGlob(pattern string) (*Template, error)
func ParseFiles(filenames ...string) (*Template, error)ParseGlob si basa sulla corrispondenza di wildcard, ParseFiles si basa sui nomi di file, entrambi utilizzano il filesystem locale. Se si tratta di file html da visualizzare nel frontend, si consiglia di utilizzare il pacchetto html/template, che fornisce API coerenti con text/template, ma ha apportato elaborazioni di sicurezza per html, css, js.
