Templates en Go: text/template, html/template, FuncMap, pipelines y templates anidados

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.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP