template
Documentación oficial: template package - text/template - Go Packages
A menudo usamos la función fmt.Sprintf para formatear cadenas, pero solo es adecuada para manejar cadenas pequeñas y requiere el uso de verbos de formato para especificar el tipo. No admite la nomenclatura de parámetros ni el manejo de situaciones complejas. Este es el problema que el motor de plantillas necesita resolver. Por ejemplo, las páginas HTML estáticas enlazadas al backend necesitan usar un motor de plantillas. Hay muchas bibliotecas de motores de plantillas de terceros excelentes en la comunidad, como pongo2, sprig, jet. Sin embargo, el protagonista de este artículo es la biblioteca de motor de plantillas incorporada de Go text/template. En el desarrollo real, generalmente se usa html/template, que se basa en el primero y realiza muchos tratamientos de seguridad relacionados con HTML. En general, se puede usar el primero, pero si se trata de un manejo de plantillas relacionado con HTML, se recomienda usar el segundo para mayor seguridad.
Inicio rápido
A continuación se muestra un ejemplo simple de uso del motor de plantillas, como se muestra a continuación
package main
import (
"fmt"
"os"
"text/template"
)
func main() {
tmpl := `Esta es la primera cadena de plantilla, {{ .message }}`
te, err := template.New("texTmpl").Parse(tmpl)
if err != nil {
fmt.Println(err)
return
}
data := map[string]any{
"message": "¡hola mundo!",
}
execErr := te.Execute(os.Stdout, data)
if execErr != nil {
fmt.Println(err)
}
}La salida del código anterior es
Esta es la primera cadena de plantilla, ¡hola mundo!En el código del caso, tmpl es una cadena de plantilla, y {{ .message }} en la cadena es un parámetro de plantilla del motor de plantillas. Primero, la cadena de plantilla se analiza a través del método *Template.Parse,
func (t *Template) Parse(text string) (*Template, error)Después de analizar con éxito, los datos de data se aplican a la plantilla a través del método *Template.Execute, y finalmente se generan al Writer pasado, es decir, os.Stdout.
func (t *Template) Execute(wr io.Writer, data any) errorEn el uso futuro del motor de plantillas, básicamente hay tres pasos:
- Obtener la plantilla
- Analizar la plantilla
- Aplicar los datos a la plantilla
Como se puede ver, el uso del motor de plantillas es bastante simple. Lo que es un poco más complejo es la sintaxis de plantilla del motor de plantillas, que es el contenido principal que este artículo explicará.
Sintaxis de plantilla
Parámetros
Go usa dos pares de llaves {{ }} para indicar un parámetro de plantilla en la plantilla, y usa . para representar el objeto raíz. El objeto raíz es el data pasado. Es como acceder a la variable miembro de un tipo, y se puede acceder al valor correspondiente en la plantilla conectando el nombre de la variable con el símbolo ., por ejemplo
{{ .data }}Siempre que exista una variable miembro con el mismo nombre, de lo contrario se producirá un error. Para el data pasado, generalmente es una estructura o map, o puede ser un tipo básico, como una cadena numérica. En este caso, el objeto raíz representado por . es él mismo. Dentro de las llaves, no es necesario acceder al objeto raíz para obtener el valor, también puede ser un literal de tipo básico, por ejemplo
{{ 1 }}
{{ 3.14 }}
{{ "jack" }}Independientemente del tipo, finalmente se obtendrá su representación de cadena a través de fmt.Sprintf("%s", val). Vea el siguiente ejemplo.
func main() {
out := os.Stdout
tmpl := "data-> {{ . }}\n"
datas := []any{
"¡hola mundo!",
6379,
3.1415926,
[]any{1, "2*2", 3.6},
map[string]any{"data": "¡hola mundo!"},
struct {
Data string
}{Data: "¡hola mundo!"},
}
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)
}La salida es la siguiente
data-> ¡hola mundo!
data-> 6379
data-> 3.1415926
data-> [1 2*2 3.6]
data-> map[data:¡hola mundo!]
data-> {¡hola mundo!}Como se puede ver, la forma de salida es consistente con el uso directo de fmt.Sprintf. Para estructuras y maps, se puede acceder a su valor a través del nombre del campo, como se muestra a continuación
func main() {
out := os.Stdout
tmpl := "data-> {{ .Data }}\n"
datas := []any{
map[string]any{"Data": "¡hola mundo!"},
struct {
Data string
}{Data: "¡hola mundo!"},
}
for _, data := range datas {
err := ExecTmpl(out, tmpl, data)
if err != nil {
panic(err)
}
}
}La salida es la siguiente
data-> ¡hola mundo!
data-> ¡hola mundo!Para slices y map, aunque no proporciona una sintaxis específica para acceder al valor de un índice, se puede lograr a través de una llamada a función, como se muestra a continuación
func main() {
out := os.Stdout
tmpl := "data-> {{ index . 1}}\n"
datas := []any{
[]any{"primero", "segundo"},
map[int]any{1: "primero"},
}
for _, data := range datas {
err := ExecTmpl(out, tmpl, data)
if err != nil {
panic(err)
}
}
}Salida
data-> segundo
data-> primeroSi es un slice multidimensional, se puede acceder al valor del índice correspondiente de la siguiente manera, equivalente a s[i][j][k]
{{ index . i j k }}Para estructuras o maps anidados, se puede usar .k1.k2.k3 para acceder, por ejemplo
{{ .person.father.name }}Al usar parámetros de plantilla, se puede agregar el símbolo - antes y después del parámetro para eliminar el espacio en blanco antes y después del parámetro. Vea un ejemplo
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, el resultado debería ser 10 > 2, pero debido a que se agregaron símbolos - antes y después del parámetro op, los espacios en blanco antes y después se eliminarán, por lo que la salida real es
10>2Cabe señalar que en las llaves, el símbolo - debe estar separado del parámetro por un espacio, es decir, debe estar en el formato {{- . -}}. La razón por la que se escribe en el formato {{ - . - }} con espacios adicionales en ambos lados en el ejemplo es puramente porque se ve mejor, en realidad no hay tal restricción de sintaxis.
Comentarios
La sintaxis de plantilla admite comentarios. Los comentarios no se generarán en la plantilla final. Su sintaxis es la siguiente
{{/* este es un comentario */}}Los símbolos de comentario /* y */ deben estar adyacentes a las llaves, y no puede haber otros caracteres entre ellos, de lo contrario no se podrán analizar correctamente. Solo hay una excepción, que es cuando se eliminan los espacios en blanco
{{- /* este es un comentario */ -}}Variables
Las variables también se pueden declarar en la plantilla, el símbolo $ se usa para indicar que es una variable, y se usa := para la asignación, al igual que el código de Go. El ejemplo es el siguiente.
{{ $name := .Name }}
{{ $val := index . 1 }}
{{ $val := index .dict key }}
// Asignación de entero
{{ $numer := 1 }}
// Asignación de punto flotante
{{ $float := 1.234}}
// Asignación de cadena
{{ $name := "jack" }}En el uso posterior, se accede al valor de la variable a través de $ seguido del nombre de la variable, por ejemplo
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)
}
}
}Salida
jackLas variables deben declararse antes de usarlas, de lo contrario se mostrará undefined variable, y también deben usarse dentro del ámbito.
Funciones
La sintaxis de la plantilla en sí no es mucha, la mayoría de las funciones se implementan a través de funciones. El formato de llamada a función es el nombre de la función seguido de la lista de parámetros, separados por espacios, como se muestra a continuación
{{ funcname arg1 arg2 arg3 ... }}Por ejemplo, la función index usada anteriormente
{{ index .s 1 }}La función eq usada para comparar si son iguales
{{ eq 1 2 }}Cada *Template tiene un FuncsMap para registrar el mapeo de funciones
type FuncMap map[string]anyAl crear una plantilla, se obtiene la tabla de mapeo de funciones predeterminada de text/template.builtins. A continuación se muestran todas las funciones incorporadas
| Nombre de función | Función | Ejemplo |
|---|---|---|
and | Operación AND | {{ and true false }} |
or | Operación OR | {{ or true false }} |
not | Operación NOT | {{ not true }} |
eq | Si son iguales | {{ eq 1 2 }} |
ne | Si no son iguales | {{ ne 1 2 }} |
lt | Menor que | {{ lt 1 2 }} |
le | Menor o igual que | {{ le 1 2 }} |
gt | Mayor que | {{ gt 1 2 }} |
ge | Mayor o igual que | {{ ge 1 2 }} |
len | Devuelve la longitud | {{ len .slice }} |
index | Obtiene el elemento del índice especificado del objetivo | {{ 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 de consulta de url | {{ urlquery .query }} |
Además de estos, hay una función incorporada más especial call, que se usa para llamar directamente a la función en los data pasados durante el período Execute. Por ejemplo, la siguiente plantilla
{{ call .string 1024 }}Los datos pasados son los siguientes
map[string]any{
"string": func(val any) string { return fmt.Sprintf("%v: 2048", val) },
}Entonces se generará en la plantilla
1024: 2048Esta es una de las formas de personalizar funciones, pero generalmente se recomienda usar el método *Template.Funcs para agregar funciones personalizadas, porque este último puede actuar globalmente y no necesita vincularse al objeto raíz.
func (t *Template) Funcs(funcMap FuncMap) *TemplateEl valor de retorno de una función personalizada generalmente tiene dos, el primero es el valor de retorno que se necesita usar, y el segundo es error. Por ejemplo, hay la siguiente función personalizada
template.FuncMap{
"add": func(val any) (string, error) { return fmt.Sprintf("%v+1", val), nil },
}Luego úsela directamente en la plantilla
{{ add 1024 }}Su resultado es
1024 + 1Pipeline
Este pipeline es diferente de chan. La documentación oficial lo llama pipeline, y cualquier operación que pueda generar datos se llama pipeline. Las siguientes operaciones de plantilla pertenecen a operaciones de pipeline
{{ 1 }}
{{ eq 1 2 }}
{{ $name }}
{{ .name }}Los que están familiarizados con Linux deben conocer el operador de pipeline |. La plantilla también admite tal escritura. Las operaciones de pipeline aparecen con frecuencia en las plantillas, por ejemplo
{{ $name := 1 }}{{ $name | print | printf "%s+1=?" }}Su resultado es
1+1=?También se usará con frecuencia en with, if, range posteriores.
with
La declaración with puede controlar el ámbito de las variables y el objeto raíz. El formato es el siguiente
{{ with pipeline }}
text
{{ end }}with verificará el valor devuelto por la operación de pipeline. Si el valor está vacío, la plantilla text intermedia no se generará. Si desea manejar el caso vacío, puede usar with else. El formato es el siguiente
{{ with pipeline }}
text1
{{ else }}
text2
{{ end }}Si el valor devuelto por la operación de pipeline está vacío, se ejecutará la lógica de este bloque else. Las variables declaradas en la declaración with tienen un ámbito limitado a la declaración with. Vea el siguiente ejemplo
{{ $name := "mike" }}
{{ with $name := "jack" }}
{{- $name -}}
{{ end }}
{{- $name -}}Su salida es la siguiente. Obviamente, esto se debe a que los ámbitos son diferentes y son dos variables diferentes.
jackmikeLa declaración with también puede reescribir el objeto raíz dentro del ámbito, como se muestra a continuación
{{ with .name }}
nombre: {{- .second }}-{{ .first -}}
{{ end }}
edad: {{ .age }}
dirección: {{ .address }}Pase los siguientes datos
map[string]any{
"name": map[string]any{
"first": "jack",
"second": "bob",
},
"age": 1,
"address": "usa",
}Su salida
nombre:bob-jack
edad: 1
dirección: usaComo se puede ver, dentro de la declaración with, el objeto raíz . se ha convertido en .name.
Condición
El formato de la declaración condicional es el siguiente
{{ if pipeline }}
text1
{{ else if pipeline }}
text2
{{ else }}
text3
{{ end }}Al igual que escribir código ordinario, es muy fácil de entender. A continuación se muestran algunos ejemplos simples,
{{ if eq .lang "en" }}
{{- .content.en -}}
{{ else if eq .lang "zh" }}
{{- .content.zh -}}
{{ else }}
{{- .content.fallback -}}
{{ end }}Los datos pasados
map[string]any{
"lang": "zh",
"content": map[string]any{
"en": "¡hola, mundo!",
"zh": "¡hola, mundo!",
"fallback": "¡hola, mundo!",
},
}La plantilla en el ejemplo decide cómo mostrar el contenido según el idioma lang pasado. Resultado de la salida
¡hola, mundo!Iteración
El formato de la declaración de iteración es el siguiente. El range admitido debe ser un array, slice, map o channel.
{{ range pipeline }}
cuerpo del bucle
{{ end }}Úselo junto con else. Cuando la longitud es 0, se ejecutará el contenido del bloque else.
{{ range pipeline }}
cuerpo del bucle
{{ else }}
fallback
{{ end }}Además, también admite operaciones como break y continue, por ejemplo
{{ range pipeline }}
{{ if pipeline }}
{{ break }}
{{ end }}
{{ if pipeline }}
{{ continue }}
{{ end }}
cuerpo del bucle
{{ end }}A continuación se muestra un ejemplo de iteración.
{{ range $index, $val := . }}
{{- if eq $index 0 }}
{{- continue -}}
{{ end -}}
{{- $index}}: {{ $val }}
{{ end }}Datos pasados
[]any{1, "2", 3.14},Salida
1: 2
2: 3.14Iterar sobre un map es lo mismo.
Anidamiento
Se pueden definir múltiples plantillas en una plantilla, por ejemplo
{{ define "t1" }} t1 {{ end }}
{{ define "t2" }} t2 {{ end }}Estas plantillas definidas no se generarán en la plantilla final, a menos que se especifique el nombre al cargar o se especifique manualmente a través de la declaración template.
func (t *Template) ExecuteTemplate(wr io.Writer, name string, data any) errorPor ejemplo, el siguiente caso
{{ define "t1" }}
{{- with .t1 }}
{{- .data -}}
{{ end -}}
{{ end }}
{{ define "t2" }}
{{- with .t2 }}
{{- .data -}}
{{ end}}
{{ end -}}Pase los siguientes datos
map[string]any{
"t1": map[string]any{"data": "cuerpo de plantilla 1"},
"t2": map[string]any{"data": "cuerpo de plantilla 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": "cuerpo de plantilla 1"},
"t2": map[string]any{"data": "cuerpo de plantilla 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)
}Salida
cuerpo de plantilla 1O también puede especificar manualmente la plantilla
{{ define "t1" }}
{{- with .t1 }}
{{- .data -}}
{{ end -}}
{{ end }}
{{ define "t2" }}
{{- with .t2 }}
{{- .data -}}
{{ end}}
{{ end -}}
{{ template "t2" .}}Entonces, ya sea que se especifique el nombre de la plantilla al analizar, se cargará t2.
Asociación
Las subplantillas solo declaran múltiples plantillas con nombre dentro de una plantilla. La asociación es asociar múltiples *Template con nombre externos. Luego se hace referencia a la plantilla especificada a través de la declaración template.
{{ template "templateName" pipeline}}pipeline puede especificar el objeto raíz de la plantilla de asociación según sus necesidades, o también puede pasar directamente el objeto raíz de la plantilla actual. Vea el siguiente ejemplo de código
func main() {
tmpl1 := `nombre: {{ .name }}`
tmpl2 := `edad: {{ .age }}`
tmpl3 := `Información de la persona
{{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
}En el código anterior, t3 está asociada con t1 y t2, y se usa el método *Template.AddParseTree para asociar
func (t *Template) AddParseTree(name string, tree *parse.Tree) (*Template, error)El resultado final de la generación de la plantilla es
Información de la persona
nombre: jack
edad: 18Slots
A través de la declaración block, se puede lograr un efecto similar al slot de Vue, cuyo propósito es reutilizar una plantilla. Vea un caso de uso para saber cómo usarlo. En la plantilla t1, defina el slot
Información básica de la persona
nombre: {{ .name }}
edad: {{ .age }}
dirección: {{ .address }}
{{ block "slot" . }} contenido predeterminado {{ end }}La declaración block puede tener contenido predeterminado en el slot. Cuando otros templates usan el slot posteriormente, el contenido predeterminado se sobrescribirá. En la plantilla t2, haga referencia a la plantilla t1 y use define para definir el contenido incrustado
{{ template "person.txt" . }}
{{ define "slot" }}
escuela: {{ .school }}
{{ end }}Después de asociar las dos plantillas, pase los siguientes datos
map[string]any{
"name": "jack",
"age": 18,
"address": "usa",
"company": "google",
"school": "mit",
}El resultado final de la salida es
Información básica de la persona
nombre: jack
edad: 18
dirección: usa
escuela: mitArchivos de plantilla
En los casos de sintaxis de plantilla, se usan literales de cadena como plantillas. En el uso real, la mayoría de las plantillas se colocan en archivos.
func ParseFS(fsys fs.FS, patterns ...string) (*Template, error)Por ejemplo, template.ParseFs carga plantillas que coinciden con pattern desde el sistema de archivos especificado. El siguiente ejemplo usa embed.FS como sistema de archivos y prepara tres archivos
# person.txt
Información básica de la persona
nombre: {{ .name }}
edad: {{ .age }}
dirección: {{ .address }}
{{ block "slot" . }} {{ end }}
# student.txt
{{ template "person.txt" . }}
{{ define "slot" }}
escuela: {{ .school }}
{{ end }}
# employee.txt
{{ template "person.txt" . }}
{{ define "slot" }}
empresa: {{ .company }}
{{ end }}El código es el siguiente
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)
}La salida es
Información básica de la persona
nombre: jack
edad: 18
dirección: usa
escuela: mit
Información básica de la persona
nombre: jack
edad: 18
dirección: usa
empresa: googleEste es un caso de uso muy simple de archivo de plantilla. person.txt se usa como archivo de slot, y los otros dos reutilizan su contenido e incrustan nuevo contenido personalizado. También se pueden usar las siguientes dos funciones
func ParseGlob(pattern string) (*Template, error)
func ParseFiles(filenames ...string) (*Template, error)ParseGlob se basa en la coincidencia de comodines, ParseFiles se basa en nombres de archivo, y ambos usan el sistema de archivos local. Si se usa para mostrar archivos html en el frontend, se recomienda usar el paquete html/template. Su API es completamente consistente con text/template, pero realiza tratamientos de seguridad para html, css y js.
