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ć.