Bem-vindo à aula 59 do nosso curso de Go! Hoje vamos mergulhar no pacote html/template, uma das ferramentas mais importantes para criar páginas web dinâmicas em Go. Diferente de simplesmente concatenar strings, o html/template oferece um mecanismo robusto para gerar HTML com segurança, evitando ataques de injeção de código (XSS) por meio de escape automático.

Nesta aula, você aprenderá desde o básico de parsing e execução até técnicas avançadas de composição de templates. Vamos explorar como passar dados para os templates, como o escape automático protege seu site e como organizar templates reutilizáveis. Ao final, você terá uma base sólida para criar interfaces web dinâmicas e seguras em Go.

Parsing e execução

O primeiro passo para usar templates é fazer o parsing do conteúdo. O pacote html/template oferece funções como template.New, template.ParseFiles e template.ParseGlob para carregar templates de strings ou arquivos. O parsing analisa o texto do template e o converte em uma estrutura interna que pode ser executada.

Após o parsing, a execução é feita com o método Execute, que recebe um io.Writer (geralmente um http.ResponseWriter ou um buffer) e os dados que serão injetados no template. Exemplo básico:

package main

import (
    "html/template"
    "os"
)

func main() {
    // Template simples
    t := template.New("exemplo")
    t, _ = t.Parse("Olá, {{.Nome}}!")

    // Dados
    dados := struct{ Nome string }{"Maria"}

    // Executa e escreve na saída padrão
    t.Execute(os.Stdout, dados)
}

Nesse exemplo, criamos um template com a string "Olá, {{.Nome}}!" e executamos com um struct contendo o campo Nome. O resultado será "Olá, Maria!". Se você estiver criando um servidor web, o http.ResponseWriter é o writer comum.

É importante notar que html/template é um pacote separado de text/template, mas com a mesma API básica. A diferença crucial é que html/template aplica escape automático para contextos HTML, o que veremos a seguir.

Variáveis

No sistema de templates, você pode acessar os dados passados na execução através de . (ponto). O ponto representa o valor passado como data no Execute. Você pode acessar campos de structs, chaves de mapas e métodos.

Além disso, é possível criar variáveis locais dentro do template usando $. Por exemplo:

// Template
const tpl = `
{{$nome := .Nome}}
Olá, {{$nome}}!
`

t := template.Must(template.New("var").Parse(tpl))
dados := map[string]string{"Nome": "João"}
t.Execute(os.Stdout, dados)

Nesse caso, definimos uma variável $nome que recebe o valor de .Nome e depois a usamos. Isso é útil para evitar repetição e para armazenar valores temporários.

Também podemos usar funções em templates. O pacote html/template já inclui algumas funções embutidas, como html, js, urlquery, e você pode adicionar funções personalizadas com Funcs. Por exemplo:

funcs := template.FuncMap{
    "upper": strings.ToUpper,
}
t := template.New("func").Funcs(funcs)
t, _ = t.Parse("Olá, {{upper .Nome}}!")
t.Execute(os.Stdout, dados)

Isso permite transformar dados antes de exibi-los, como colocar o nome em maiúsculas.

Escape automático

Uma das principais vantagens do html/template é o escape automático. Quando você usa {{.Campo}}, o pacote analisa o contexto em que a ação está inserida (HTML, atributo, JavaScript, CSS, URL) e aplica o escape adequado. Isso previne ataques de XSS (Cross-Site Scripting), pois caracteres especiais como <, >, &, aspas e outros são convertidos para entidades HTML seguras.

Por exemplo, se um usuário enviar o nome <script>alert('xss')</script>, ao exibi-lo com {{.Nome}}, o template irá gerar &lt;script&gt;alert('xss')&lt;/script&gt;, que será exibido como texto, não executado.

O escape é contextual: se você colocar uma variável dentro de um atributo HTML, ele escapará aspas de forma apropriada. Se colocar dentro de um bloco JavaScript, ele escapará caracteres específicos de JS. Isso é feito automaticamente, sem que você precise chamar funções de escape manualmente.

Porém, em situações onde você precisa confiar em HTML gerado por terceiros (como markdown), você pode usar o tipo template.HTML para desabilitar o escape. Mas isso deve ser feito com extrema cautela, pois abre brechas de segurança.

// Cuidado: usar template.HTML pode ser perigoso
conteudo := "<b>Texto em negrito</b>"
t, _ := template.New("safe").Parse("{{.Conteudo}}")
dados := struct{ Conteudo template.HTML }{template.HTML(conteudo)}
t.Execute(os.Stdout, dados)

Nesse caso, o conteúdo será interpretado como HTML. Use apenas quando tiver certeza de que o conteúdo é seguro.

Composição

Em aplicações reais, é comum ter vários templates que se repetem, como cabeçalho, rodapé, menus. Para evitar duplicação, o html/template permite compor templates de várias formas:

  • Definição de blocos: você pode definir blocos com {{define "nome"}}...{{end}} e depois incluí-los com {{template "nome" .}}.
  • Parsing de múltiplos arquivos: com ParseFiles ou ParseGlob, você carrega vários templates em um único Template e pode referenciá-los pelo nome.
  • Herança de layout: usando block e define, você pode criar layouts base e substituir partes específicas.

Exemplo de composição com ParseFiles:

// layout.html
{{define "layout"}}
<!DOCTYPE html>
<html>
<head><title>Meu Site</title></head>
<body>
    {{template "conteudo" .}}
</body>
</html>
{{end}}

// conteudo.html
{{define "conteudo"}}
<h1>Olá, {{.Nome}}!</h1>
{{end}}

// main.go
t, err := template.ParseFiles("layout.html", "conteudo.html")
if err != nil { panic(err) }
t.ExecuteTemplate(os.Stdout, "layout", dados)

Nesse exemplo, o template layout define a estrutura HTML e chama o bloco conteudo. O método ExecuteTemplate executa um template específico pelo nome.

Com essa abordagem, você pode criar sistemas de layout flexíveis e reutilizáveis, mantendo o código organizado.

Boas práticas

Ao trabalhar com templates em Go, algumas boas práticas ajudam a manter a segurança e a manutenibilidade:

  • Sempre use html/template em vez de text/template para HTML, pois ele oferece escape automático.
  • Evite usar template.HTML a menos que seja absolutamente necessário e você tenha controle sobre o conteúdo.
  • Organize seus templates em arquivos separados e use ParseFiles ou ParseGlob para carregá-los.
  • Use template.Must para garantir que o parsing não falhe silenciosamente.
  • Prefira passar dados estruturados (structs) em vez de mapas, para ter segurança de tipos.
  • Teste seus templates com dados de exemplo, incluindo inputs maliciosos, para verificar o escape.

Referências

Exercícios

  1. Crie um programa que carregue um template de arquivo chamado index.html com o conteúdo <h1>Hello, {{.Name}}!</h1> e execute com um struct contendo Name igual a "Alice". Mostre o resultado.

    ✓ Resposta:
    package main
    
    import (
        "html/template"
        "os"
    )
    
    type PageData struct {
        Name string
    }
    
    func main() {
        t, err := template.ParseFiles("index.html")
        if err != nil {
            panic(err)
        }
        data := PageData{Name: "Alice"}
        t.Execute(os.Stdout, data)
    }
  2. Escreva um template que use uma variável local para armazenar a idade e exiba "Idade: X anos". Execute com dados de exemplo.

    ✓ Resposta:
    package main
    
    import (
        "html/template"
        "os"
    )
    
    func main() {
        const tpl = `
    {{$idade := .Idade}}
    Idade: {{$idade}} anos
    `
        t := template.Must(template.New("var").Parse(tpl))
        data := struct{ Idade int }{30}
        t.Execute(os.Stdout, data)
    }
  3. Demonstre o escape automático exibindo a string <script>alert('xss')</script> em um template e mostre o HTML gerado. O que acontece?

    ✓ Resposta: O template escapa os caracteres especiais, gerando &lt;script&gt;alert('xss')&lt;/script&gt;. No navegador, isso é exibido como texto, não executado. Exemplo:
    package main
    
    import (
        "html/template"
        "os"
    )
    
    func main() {
        t := template.Must(template.New("escape").Parse("{{.}}"))
        t.Execute(os.Stdout, "<script>alert('xss')</script>")
    }
  4. Crie um layout principal com cabeçalho e rodapé fixos, e um bloco de conteúdo que pode ser substituído. Use ParseFiles com dois arquivos: layout.html e pagina.html.

    ✓ Resposta:
    // layout.html
    {{define "layout"}}
    <!DOCTYPE html>
    <html>
    <head><title>Meu Site</title></head>
    <body>
    <header><h1>Cabeçalho</h1></header>
    {{template "conteudo" .}}
    <footer><p>Rodapé</p></footer>
    </body>
    </html>
    {{end}}
    
    // pagina.html
    {{define "conteudo"}}
    <h2>Página: {{.Titulo}}</h2>
    {{end}}
    
    // main.go
    package main
    
    import (
        "html/template"
        "os"
    )
    
    func main() {
        t := template.Must(template.ParseFiles("layout.html", "pagina.html"))
        data := struct{ Titulo string }{"Home"}
        t.ExecuteTemplate(os.Stdout, "layout", data)
    }
  5. Adicione uma função personalizada que converte uma string para maiúsculas e use-a em um template para exibir o nome do usuário.

    ✓ Resposta:
    package main
    
    import (
        "html/template"
        "os"
        "strings"
    )
    
    func main() {
        funcs := template.FuncMap{"upper": strings.ToUpper}
        t := template.New("func").Funcs(funcs)
        t, _ = t.Parse("Olá, {{upper .Nome}}!")
        data := struct{ Nome string }{"maria"}
        t.Execute(os.Stdout, data)
    }