Go incluye en su librería estándar dos paquetes de templates: text/template para texto genérico y html/template para HTML con escaping automático anti-XSS. Comparten la misma sintaxis pero el segundo entiende el contexto HTML y escapa el contenido dinámico según dónde aparezca.
Sintaxis básica
package main
import (
"html/template"
"os"
)
type Usuario struct {
Nombre string
Email string
Admin bool
Roles []string
}
const tmpl = `<!DOCTYPE html>
<html>
<body>
<h1>Bienvenido, {{.Nombre}}</h1>
<p>Email: {{.Email}}</p>
{{if .Admin}}<p class="badge">Administrador</p>{{end}}
{{if .Roles}}
<ul>
{{range .Roles}}<li>{{.}}</li>{{end}}
</ul>
{{end}}
</body>
</html>`
func main() {
t := template.Must(template.New("usuario").Parse(tmpl))
u := Usuario{
Nombre: "Ana García",
Email: "[email protected]",
Admin: true,
Roles: []string{"editor", "moderador"},
}
t.Execute(os.Stdout, u)
}
Escaping automático en html/template
La diferencia clave con text/template es que html/template entiende el contexto donde está el dato y aplica el escape correcto. Si un usuario introduce <script>alert(1)</script> como nombre, el paquete lo convierte a entidades HTML automáticamente:
// Con html/template, esto es seguro:
{{.NombreUsuario}} // ? <script>alert(1)</script>
// Para contenido HTML confiable (generado por ti, no por el usuario):
import "html/template"
type Pagina struct {
Titulo string
Contenido template.HTML // marca el contenido como seguro
}
datos := Pagina{
Titulo: "Mi página",
Contenido: template.HTML("<strong>negrita real</strong>"),
}
Pipelines
Los pipelines encadenan funciones con el operador |. El resultado de cada elemento se pasa como último argumento al siguiente:
// Encadenar: upper, truncar, formatear
{{.Nombre | upper | printf "Usuario: %s"}}
// Con condición
{{.Precio | printf "%.2f "}}
// Acceso a campos anidados con pipeline
{{with .Direccion}}
{{.Calle}}, {{.Ciudad}}
{{end}}
FuncMap: añadir funciones propias
import (
"html/template"
"strings"
"time"
)
funciones := template.FuncMap{
"upper": strings.ToUpper,
"lower": strings.ToLower,
"truncar": func(s string, n int) string {
if len(s) <= n {
return s
}
return s[:n] + "..."
},
"formatFecha": func(t time.Time) string {
return t.Format("02/01/2006")
},
"pluralizar": func(n int, singular, plural string) string {
if n == 1 {
return singular
}
return plural
},
}
t := template.New("pagina").Funcs(funciones)
t, err := t.ParseFiles("templates/pagina.html")
// En el template:
// {{.Nombre | upper | truncar 20}}
// {{.FechaCreacion | formatFecha}}
// {{len .Comentarios}} {{len .Comentarios | pluralizar "comentario" "comentarios"}}
Templates anidados con define y ParseGlob
define nombra bloques reutilizables. block define un bloque con un valor por defecto que los templates hijos pueden sobreescribir:
// base.html
<!DOCTYPE html>
<html>
<head><title>{{block "titulo" .}}Mi Sitio{{end}}</title></head>
<body>
{{block "contenido" .}}<p>Sin contenido</p>{{end}}
</body>
</html>
// pagina.html
{{define "titulo"}}Inicio{{end}}
{{define "contenido"}}
<h1>Bienvenido, {{.Nombre}}</h1>
{{end}}
// Cargar todos los templates de una carpeta
t := template.Must(template.New("").Funcs(funciones).ParseGlob("templates/*.html"))
func renderizar(w http.ResponseWriter, nombre string, datos any) {
if err := t.ExecuteTemplate(w, nombre, datos); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
// Renderizar la página de inicio con la base
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
renderizar(w, "base.html", struct{ Nombre string }{"Ana"})
})
text/template para texto genérico
import "text/template"
// Generar un fichero de configuración
const configTmpl = `
# Configuración generada el {{.Fecha}}
server:
host: {{.Host}}
port: {{.Puerto}}
workers: {{.Workers}}
`
t := template.Must(template.New("config").Parse(configTmpl))
t.Execute(os.Stdout, struct {
Fecha string
Host string
Puerto int
Workers int
}{"2026-08-23", "localhost", 8080, 4})
Usa text/template para generar código, YAML, SQL o cualquier texto donde el escaping HTML rompería el formato. Para cualquier respuesta HTTP usa siempre html/template.
