Szablony HTML w standardowej bibliotece Go

W erze zdominowanej przez React, Vue i Next.js łatwo zapomnieć, że server-side rendering (SSR) nie wymaga frameworka JavaScript, bundlera, node_modules ani megabajtów zależności. Pakiet html/template w standardowej bibliotece Go oferuje w pełni funkcjonalny system szablonów HTML — bezpieczny, wydajny i wystarczający do budowy większości stron firmowych i aplikacji webowych.

W LinWork wszystkie nasze serwisy korzystają właśnie z tego podejścia. Oto jak organizujemy szablony w praktyce.

Podstawy — parsowanie i renderowanie

Pakiet html/template działa na prostej zasadzie: definiujesz szablon z placeholderami, przekazujesz dane i otrzymujesz gotowy HTML. Kluczowa cecha: automatyczne escapowanie — wszystkie dane wstawiane do szablonu są domyślnie zabezpieczane przed atakami XSS.

package main

import (
    "html/template"
    "net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
    tmpl := template.Must(template.ParseFiles("index.html"))
    data := map[string]string{
        "Title":   "Strona główna",
        "Content": "Witamy na naszej stronie!",
    }
    tmpl.Execute(w, data)
}

W szablonie HTML odwołujemy się do danych za pomocą podwójnych nawiasów klamrowych:

<!DOCTYPE html>
<html>
<head><title>{{.Title}}</title></head>
<body>
    <h1>{{.Content}}</h1>
</body>
</html>

Layouty — dziedziczenie szablonów

Prawdziwa moc systemu szablonów Go ujawnia się przy budowie layoutów. Zamiast kopiować nagłówek i stopkę do każdej strony, definiujemy bazowy layout z blokami, które podstrony mogą nadpisywać.

Plik layout.html:

<!DOCTYPE html>
<html lang="pl">
<head>
    <meta charset="UTF-8">
    <title>{{block "title" .}}LinWork{{end}}</title>
    <link rel="stylesheet" href="/static/style.css">
</head>
<body>
    <nav>{{template "nav" .}}</nav>
    <main>{{block "content" .}}{{end}}</main>
    <footer>{{template "footer" .}}</footer>
</body>
</html>

Plik home.html:

{{define "title"}}Strona główna — LinWork{{end}}

{{define "content"}}
<section class="hero">
    <h1>{{.Heading}}</h1>
    <p>{{.Description}}</p>
</section>
{{end}}

Renderowanie strony z layoutem:

tmpl := template.Must(template.ParseFiles(
    "templates/layout.html",
    "templates/nav.html",
    "templates/footer.html",
    "templates/home.html",
))
tmpl.ExecuteTemplate(w, "layout.html", data)

Instrukcja {{block "content" .}} definiuje blok z domyślną zawartością (tu pustą), który może być nadpisany przez {{define "content"}} w szablonie strony. To prosty, ale skuteczny mechanizm dziedziczenia.

Partiale — wielokrotne użycie komponentów

Partiale to fragmenty HTML, które można wstawiać w wielu miejscach. Typowe przykłady to nawigacja, stopka, karty produktów czy formularze.

Plik partials/card.html:

{{define "card"}}
<article class="card">
    <h3>{{.Title}}</h3>
    <p>{{.Description}}</p>
    <a href="{{.URL}}">Czytaj więcej</a>
</article>
{{end}}

Użycie w szablonie strony:

{{define "content"}}
<div class="grid">
    {{range .Projects}}
        {{template "card" .}}
    {{end}}
</div>
{{end}}

Instrukcja {{range}} iteruje po kolekcji, a {{template "card" .}} wstawia partial z przekazanym kontekstem danych (. oznacza bieżący element iteracji).

Funkcje niestandardowe — rozszerzanie szablonów

Domyślny zestaw funkcji w szablonach Go jest celowo minimalistyczny. Gdy potrzebujemy dodatkowej logiki, rejestrujemy własne funkcje za pomocą template.FuncMap:

funcMap := template.FuncMap{
    "formatDate": func(t time.Time) string {
        return t.Format("02.01.2006")
    },
    "truncate": func(s string, n int) string {
        if len(s) <= n {
            return s
        }
        return s[:n] + "..."
    },
    "safeHTML": func(s string) template.HTML {
        return template.HTML(s)
    },
}

tmpl := template.New("").Funcs(funcMap)
tmpl = template.Must(tmpl.ParseFiles("templates/layout.html", "templates/blog.html"))

Użycie w szablonie:

<time>{{formatDate .CreatedAt}}</time>
<p>{{truncate .Summary 150}}</p>

Ważna uwaga: funkcja safeHTML wyłącza automatyczne escapowanie. Używaj jej wyłącznie dla treści, którym ufasz (np. renderowany Markdown z zaufanego źródła), nigdy dla danych od użytkownika.

Embed — szablony wbudowane w binarce

Od Go 1.16 dyrektywa //go:embed pozwala wbudować pliki szablonów bezpośrednio w plik binarny aplikacji:

//go:embed templates/*
var templateFS embed.FS

func initTemplates() *template.Template {
    funcMap := template.FuncMap{ /* ... */ }
    tmpl := template.New("").Funcs(funcMap)
    tmpl = template.Must(tmpl.ParseFS(templateFS, "templates/*.html"))
    return tmpl
}

Metoda ParseFS parsuje szablony z osadzonego systemu plików. Efekt: aplikacja to jeden plik binarny, który zawiera w sobie wszystkie szablony. Nie trzeba kopiować katalogów z szablonami na serwer, nie ma ryzyka przypadkowej modyfikacji plików na produkcji.

Wydajność i cache'owanie

Szablony Go powinny być parsowane raz przy starcie aplikacji, a nie przy każdym żądaniu HTTP. Parsowanie szablonów jest operacją kosztowną — renderowanie już sparsowanego szablonu jest ekstremalnie szybkie.

// Dobrze — parsowanie raz
var templates = template.Must(template.ParseFS(templateFS, "templates/*.html"))

func handler(w http.ResponseWriter, r *http.Request) {
    templates.ExecuteTemplate(w, "home.html", data)
}

W trybie deweloperskim możesz chcieć przeładowywać szablony przy każdym żądaniu (aby widzieć zmiany bez restartu). Proste rozwiązanie to flaga -dev:

if devMode {
    tmpl, _ = template.ParseFiles("templates/layout.html", "templates/home.html")
}

Kiedy szablony Go nie wystarczą

System szablonów Go jest celowo prosty. Nie znajdziesz w nim dziedziczenia wielopoziomowego, middleware'u szablonów ani zaawansowanych dyrektyw. Jeśli potrzebujesz:

  • Interaktywności po stronie klienta — szablony Go generują statyczny HTML; do dynamicznych elementów dodaj JavaScript (htmx, Alpine.js) lub rozważ bibliotekę Templ
  • Zaawansowanego systemu komponentów — projekty takie jak Templ (natywne komponenty Go) oferują type-safety i lepsze narzędzia IDE
  • Wielojęzyczności (i18n) — potrzebujesz dodatkowej biblioteki do zarządzania tłumaczeniami

Mimo tych ograniczeń, dla stron firmowych, blogów, paneli administracyjnych i prostych aplikacji webowych — html/template w zupełności wystarcza. Brak zależności zewnętrznych, automatyczne zabezpieczenie przed XSS i zerowy koszt utrzymania to argumenty nie do przecenienia.

Chcesz zobaczyć, jak wygląda strona zbudowana w czystym Go bez frameworka? Właśnie na niej jesteś. Jeśli szukasz podobnego rozwiązania dla siebie — daj nam znać.