template
Documentação oficial: template package - text/template - Go Packages
Normalmente usamos a função fmt.Sprintf para formatar strings, mas ela só é adequada para lidar com pequenas strings e requer o uso de verbos de formatação para especificar o tipo. Não suporta nomeação de parâmetros nem situações complexas, e é exatamente isso que o motor de templates precisa resolver. Por exemplo, páginas HTML estáticas vinculadas ao backend precisam usar um motor de templates. Existem muitas bibliotecas de motores de templates de terceiros excelentes na comunidade, como pongo2, sprig, jet, mas o protagonista deste artigo é a biblioteca de motor de templates integrada do Go text/template. No desenvolvimento real, geralmente usa-se html/template, que é baseado no anterior e faz muito tratamento de segurança relacionado a HTML. Em geral, usa-se o anterior, mas se envolver processamento de templates HTML, é recomendável usar o último para maior segurança.
Início Rápido
Abaixo está um exemplo simples de uso do motor de templates, conforme mostrado:
package main
import (
"fmt"
"os"
"text/template"
)
func main() {
tmpl := `Esta é a primeira string de template, {{ .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)
}
}A saída do código acima é:
Esta é a primeira string de template, hello world!No código do caso, tmpl é uma string de template, e {{ .message }} na string é um parâmetro de template do motor de templates. Primeiro, a string de template é analisada através do método *Template.Parse:
func (t *Template) Parse(text string) (*Template, error)Após a análise bem-sucedida, os dados data são aplicados ao template através do método *Template.Execute, e finalmente a saída é enviada para o Writer passado, que é os.Stdout.
func (t *Template) Execute(wr io.Writer, data any) errorNo uso futuro do motor de templates, basicamente são estes três passos:
- Obter o template
- Analisar o template
- Aplicar os dados ao template
Como se pode ver, o uso do motor de templates é bastante simples. O que é um pouco mais complexo é a sintaxe de template do motor de templates, que é o principal conteúdo a ser explicado neste artigo.
Sintaxe de Template
Parâmetros
Go usa dois pares de chaves {{ }} para representar um parâmetro de template no template. Usa-se . para representar o objeto raiz, que são os data passados. É como acessar uma variável membro de um tipo, usando o símbolo . para concatenar o nome da variável e acessar o valor correspondente no template, por exemplo:
{{ .data }}Desde que exista uma variável membro com o mesmo nome, caso contrário ocorrerá um erro. Para os data passados, geralmente é uma struct ou map, também pode ser um tipo básico, como número ou string. Neste caso, . representa o próprio objeto. Dentro das chaves, não é necessário acessar o objeto raiz para obter o valor, também pode ser um literal de tipo básico, por exemplo:
{{ 1 }}
{{ 3.14 }}
{{ "jack" }}Independentemente do tipo, eventualmente será obtida sua representação de string através de fmt.Sprintf("%s", val). Veja o exemplo abaixo.
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)
}A saída é a seguinte:
data-> hello world!
data-> 6379
data-> 3.1415926
data-> [1 2*2 3.6]
data-> map[data:hello world!]
data-> {hello world!}Como se pode ver, a forma de saída é consistente com o uso direto de fmt.Sprintf. Para structs e maps, pode-se acessar o valor através do nome do campo, conforme mostrado abaixo:
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)
}
}
}A saída é a seguinte:
data-> hello world!
data-> hello world!Para slices e map, embora não haja uma sintaxe específica para acessar um valor de índice específico, pode-se implementar através de chamadas de função, conforme mostrado abaixo:
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)
}
}
}Saída:
data-> second
data-> firstSe for um slice multidimensional, pode-se acessar o valor do índice correspondente da seguinte forma, equivalente a s[i][j][k]:
{{ index . i j k }}Para structs ou maps aninhados, pode-se usar .k1.k2.k3 para acessar, por exemplo:
{{ .person.father.name }}Ao usar parâmetros de template, pode-se adicionar o símbolo - antes e depois do parâmetro para eliminar espaços em branco antes e depois do parâmetro. Veja um exemplo:
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, o resultado da saída deveria ser 10 > 2, mas como o símbolo - foi adicionado antes e depois do parâmetro op, os espaços em branco antes e depois serão eliminados, então a saída real é:
10>2É importante notar que nas chaves, o símbolo - deve estar separado do parâmetro por um espaço, ou seja, deve estar no formato {{- . -}}. No exemplo, a razão pela qual foi escrito no formato {{ - . - }} com espaços extras em ambos os lados é puramente por preferência pessoal de aparência, na verdade não há essa restrição de sintaxe.
Comentários
A sintaxe de template suporta comentários. Os comentários não serão gerados no template final. A sintaxe é a seguinte:
{{/* este é um comentário */}}Os símbolos de comentário /* e */ devem estar adjacentes às chaves, não pode haver outros caracteres entre eles, caso contrário não será possível analisar corretamente. Há apenas uma exceção, que é ao eliminar espaços em branco:
{{- /* este é um comentário */ -}}Variáveis
Também é possível declarar variáveis no template, usando o símbolo $ para indicar que é uma variável, e := para atribuição, assim como no código Go. Exemplo:
{{ $name := .Name }}
{{ $val := index . 1 }}
{{ $val := index .dict key }}
// Atribuição de inteiro
{{ $numer := 1 }}
// Atribuição de ponto flutuante
{{ $float := 1.234}}
// Atribuição de string
{{ $name := "jack" }}No uso subsequente, acessa-se o valor da variável concatenando o nome da variável com $, por exemplo:
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)
}
}
}Saída:
jackAs variáveis devem ser declaradas antes de serem usadas, caso contrário será exibido undefined variable, e também só podem ser usadas dentro do escopo.
Funções
A sintaxe do template em si não é muita, a maioria das funcionalidades é implementada através de funções. O formato de chamada de função é o nome da função seguido pela lista de parâmetros, separados por espaços, conforme mostrado abaixo:
{{ funcname arg1 arg2 arg3 ... }}Por exemplo, a função index usada anteriormente:
{{ index .s 1 }}A função eq usada para comparar se são iguais:
{{ eq 1 2 }}Cada *Template possui um FuncsMap para registrar o mapeamento de funções:
type FuncMap map[string]anyAo criar um template, obtém-se a tabela de mapeamento de funções padrão de text/template.builtins. Abaixo estão todas as funções integradas:
| Nome da Função | Função | Exemplo |
|---|---|---|
and | Operação E | {{ and true false }} |
or | Operação OU | {{ or true false }} |
not | Operação de negação | {{ not true }} |
eq | Se é igual | {{ eq 1 2 }} |
ne | Se não é igual | {{ ne 1 2 }} |
lt | Menor que | {{ lt 1 2 }} |
le | Menor ou igual | {{ le 1 2 }} |
gt | Maior que | {{ gt 1 2 }} |
ge | Maior ou igual | {{ ge 1 2 }} |
len | Retorna o comprimento | {{ len .slice }} |
index | Obtém o elemento do índice especificado do alvo | {{ 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 }} |
Além destas, há uma função integrada especial call, que é usada para chamar diretamente uma função nos data passados durante o período Execute. Por exemplo, o template abaixo:
{{ call .string 1024 }}Os dados passados são:
map[string]any{
"string": func(val any) string { return fmt.Sprintf("%v: 2048", val) },
}Então será gerado no template:
1024: 2048Esta é uma das formas de personalizar funções, mas geralmente recomenda-se usar o método *Template.Funcs para adicionar funções personalizadas, pois este pode atuar globalmente sem precisar vincular ao objeto raiz.
func (t *Template) Funcs(funcMap FuncMap) *TemplateO valor de retorno de uma função personalizada geralmente tem dois valores: o primeiro é o valor de retorno necessário e o segundo é error. Por exemplo, há a seguinte função personalizada:
template.FuncMap{
"add": func(val any) (string, error) { return fmt.Sprintf("%v+1", val), nil },
}E então usa-se diretamente no template:
{{ add 1024 }}O resultado é:
1024 + 1Pipeline
Este pipeline é diferente de chan. A documentação oficial o chama de pipeline, e qualquer operação que possa gerar dados é chamada de pipeline. As seguintes operações de template pertencem a operações de pipeline:
{{ 1 }}
{{ eq 1 2 }}
{{ $name }}
{{ .name }}Quem está familiarizado com Linux deve conhecer o operador de pipeline |. O template também suporta essa sintaxe. Operações de pipeline frequentemente aparecem em templates, por exemplo:
{{ $name := 1 }}{{ $name | print | printf "%s+1=?" }}O resultado é:
1+1=?Também será frequentemente usado em with, if, range subsequentes.
With
Através da declaração with, pode-se controlar o escopo de variáveis e do objeto raiz. O formato é o seguinte:
{{ with pipeline }}
text
{{ end }}with verifica o valor retornado pela operação de pipeline. Se o valor for vazio, o template text no meio não será gerado. Se quiser lidar com o caso vazio, pode-se usar with else. O formato é o seguinte:
{{ with pipeline }}
text1
{{ else }}
text2
{{ end }}Se o valor retornado pela operação de pipeline for vazio, então a lógica do bloco else será executada. As variáveis declaradas na declaração with têm seu escopo limitado apenas à declaração with. Veja o exemplo abaixo:
{{ $name := "mike" }}
{{ with $name := "jack" }}
{{- $name -}}
{{ end }}
{{- $name -}}A saída é a seguinte, obviamente devido a escopos diferentes, são duas variáveis diferentes:
jackmikeAtravés da declaração with, também é possível reescrever o objeto raiz dentro do escopo, conforme mostrado abaixo:
{{ with .name }}
name: {{- .second }}-{{ .first -}}
{{ end }}
age: {{ .age }}
address: {{ .address }}Passando os seguintes dados:
map[string]any{
"name": map[string]any{
"first": "jack",
"second": "bob",
},
"age": 1,
"address": "usa",
}A saída:
name:bob-jack
age: 1
address: usaComo se pode ver, dentro da declaração with, o objeto raiz . já se tornou .name.
Condições
O formato da declaração de condição é o seguinte:
{{ if pipeline }}
text1
{{ else if pipeline }}
text2
{{ else }}
text3
{{ end }}Assim como escrever código comum, é muito fácil de entender. Abaixo estão alguns exemplos simples:
{{ if eq .lang "en" }}
{{- .content.en -}}
{{ else if eq .lang "zh" }}
{{- .content.zh -}}
{{ else }}
{{- .content.fallback -}}
{{ end }}Os dados passados:
map[string]any{
"lang": "zh",
"content": map[string]any{
"en": "hello, world!",
"zh": "你好,世界!",
"fallback": "hello, world!",
},
}O template no exemplo decide como exibir o conteúdo com base no lang de idioma passado. Resultado da saída:
你好,世界!Iteração
O formato da declaração de iteração é o seguinte. O range suporta pipeline deve ser array, slice, map e channel:
{{ range pipeline }}
loop body
{{ end }}Usando em conjunto com else, quando o comprimento for 0, o conteúdo do bloco else será executado:
{{ range pipeline }}
loop body
{{ else }}
fallback
{{ end }}Além disso, também suporta operações como break e continue, por exemplo:
{{ range pipeline }}
{{ if pipeline }}
{{ break }}
{{ end }}
{{ if pipeline }}
{{ continue }}
{{ end }}
loop body
{{ end }}Abaixo está um exemplo de iteração:
{{ range $index, $val := . }}
{{- if eq $index 0 }}
{{- continue -}}
{{ end -}}
{{- $index}}: {{ $val }}
{{ end }}Dados passados:
[]any{1, "2", 3.14},Saída:
1: 2
2: 3.14Iterar sobre map também é o mesmo princípio.
Aninhamento
Pode-se definir múltiplos templates em um template, por exemplo:
{{ define "t1" }} t1 {{ end }}
{{ define "t2" }} t2 {{ end }}Estes templates definidos não serão gerados no template final, a menos que seja especificado um nome durante o carregamento ou especificado manualmente através da declaração template.
func (t *Template) ExecuteTemplate(wr io.Writer, name string, data any) errorPor exemplo, o caso abaixo:
{{ define "t1" }}
{{- with .t1 }}
{{- .data -}}
{{ end -}}
{{ end }}
{{ define "t2" }}
{{- with .t2 }}
{{- .data -}}
{{ end}}
{{ end -}}Passando os seguintes dados:
map[string]any{
"t1": map[string]any{"data": "template body 1"},
"t2": map[string]any{"data": "template body 2"},
}Código:
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)
}Saída:
template body 1Ou também é possível especificar manualmente o template:
{{ define "t1" }}
{{- with .t1 }}
{{- .data -}}
{{ end -}}
{{ end }}
{{ define "t2" }}
{{- with .t2 }}
{{- .data -}}
{{ end}}
{{ end -}}
{{ template "t2" .}}Então, ao analisar, independentemente de especificar o nome do template, t2 será carregado.
Associação
Sub-templates são apenas múltiplos templates nomeados declarados dentro de um template. Associação é vincular múltiplos *Template nomeados externos. E então referencia-se o template especificado através da declaração template.
{{ template "templateName" pipeline}}O pipeline pode especificar o objeto raiz do template associado de acordo com suas necessidades, ou também pode passar diretamente o objeto raiz do template atual. Veja o exemplo de código abaixo:
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
}No código acima, t3 associa t1 e t2, usando o método *Template.AddParseTree para associar:
func (t *Template) AddParseTree(name string, tree *parse.Tree) (*Template, error)O resultado final da geração do template é:
Person Info
name: jack
age: 18Slots
Através da declaração block, pode-se implementar um efeito semelhante aos slots do Vue, que é usado para reutilizar um template. Veja um caso de uso para saber como usar. No template t1, define-se um slot:
Basic Person Info
name: {{ .name }}
age: {{ .age }}
address: {{ .address }}
{{ block "slot" . }} default content body {{ end }}A declaração block pode ter conteúdo padrão no slot. Ao usar o slot em outros templates posteriormente, o conteúdo padrão será substituído. No template t2, referencia-se o template t1 e usa-se define para definir o conteúdo embutido:
{{ template "person.txt" . }}
{{ define "slot" }}
school: {{ .school }}
{{ end }}Após associar os dois templates, passa-se os seguintes dados:
map[string]any{
"name": "jack",
"age": 18,
"address": "usa",
"company": "google",
"school": "mit",
}O resultado final da saída é:
Basic Person Info
name: jack
age: 18
address: usa
school: mitArquivos de Template
Nos casos de sintaxe de template, todos usam literais de string como template. Na maioria dos casos de uso real, os templates são colocados em arquivos.
func ParseFS(fsys fs.FS, patterns ...string) (*Template, error)Por exemplo, template.ParseFs carrega templates que correspondem ao pattern do sistema de arquivos especificado. O exemplo abaixo usa embed.FS como sistema de arquivos, preparando três arquivos:
# 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 }}Código:
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)
}Saída:
Basic Person Info
name: jack
age: 18
address: usa
school: mit
Basic Person Info
name: jack
age: 18
address: usa
company: googleEste é um caso de uso muito simples de arquivo de template. person.txt serve como arquivo de slot, e os outros dois reutilizam seu conteúdo e incorporam novo conteúdo personalizado. Também é possível usar as duas funções abaixo:
func ParseGlob(pattern string) (*Template, error)
func ParseFiles(filenames ...string) (*Template, error)ParseGlob é baseado em correspondência de curinga, ParseFiles é baseado em nome de arquivo, ambos usam o sistema de arquivos local. Se for usado para exibir arquivos html no frontend, recomenda-se usar o pacote html/template, que fornece API consistente com text/template, mas faz tratamento de segurança para html, css e js.
