Go ships with everything needed to build server-rendered web applications: html/template for safe HTML generation and net/http for serving static assets. The html/template package is specifically designed to prevent cross-site scripting (XSS) — it automatically escapes values based on context (HTML, URL, CSS, JavaScript), not just a blanket escape-everything approach.
This guide covers static file serving, template syntax, caching for performance, and the template composition patterns used in real applications.
For API-focused HTTP patterns see Go building REST APIs.
Serving Static Files
http.FileServer serves an entire directory tree. Combine it with http.StripPrefix to remove the URL prefix before looking up files:
// Serve files from ./static/ at URL path /static/
// Request: GET /static/css/style.css
// File: ./static/css/style.css
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static"))))
For a single file (robots.txt, favicon.ico):
mux.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "./static/favicon.ico")
})
Security note: http.FileServer with http.Dir only serves files within that directory — path traversal (../../../etc/passwd) is blocked. But if you build paths yourself with filepath.Join(baseDir, r.URL.Path), you must validate that the result stays within baseDir.
Cache Headers for Static Assets
Static assets with a content hash in their filename (e.g., app.abc123.js) can be cached indefinitely:
mux.Handle("/static/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Long cache for hashed assets, short cache for others
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
http.StripPrefix("/static/", http.FileServer(http.Dir("./static"))).ServeHTTP(w, r)
}))
For non-hashed assets like /favicon.ico, use a shorter cache duration: max-age=3600.
HTML Templates: Use html/template, Not text/template
The html/template package wraps text/template with automatic context-aware escaping. Always use html/template for HTML output — never text/template. The difference:
// text/template: data is inserted verbatim — XSS vulnerability
// html/template: data is escaped based on context — safe by default
import "html/template"
tmpl := template.Must(template.ParseFiles("index.html"))
data := struct{ UserInput string }{"<script>alert('xss')</script>"}
tmpl.Execute(w, data)
// html/template outputs: <script>alert('xss')</script>
// text/template would output: <script>alert('xss')</script> ← DANGEROUS
The escaping is context-sensitive. Values inside <a href="{{.URL}}"> are URL-escaped; values inside <script> are JavaScript-escaped; values in HTML text nodes are HTML-escaped. The template engine tracks context as it parses.
Template Syntax
<!-- index.html -->
<!DOCTYPE html>
<html>
<head><title>{{.Title}}</title></head>
<body>
<!-- Variable output -->
<h1>{{.Heading}}</h1>
<!-- Conditionals -->
{{if .IsLoggedIn}}
<p>Welcome, {{.Username}}!</p>
{{else}}
<p><a href="/login">Log in</a></p>
{{end}}
<!-- Loops -->
<ul>
{{range .Items}}
<li>{{.Name}} — ${{printf "%.2f" .Price}}</li>
{{else}}
<li>No items available.</li>
{{end}}
</ul>
<!-- With — enters the value's scope -->
{{with .CurrentUser}}
<p>Logged in as {{.Name}} ({{.Email}})</p>
{{end}}
<!-- Pipeline — passes value to next function -->
<p>{{.Description | html | truncate 200}}</p>
</body>
</html>
Key action keywords:
{{if .Condition}}...{{else}}...{{end}}— conditionals{{range .Slice}}...{{else}}...{{end}}— loops over slices, arrays, maps;{{else}}runs when empty{{with .Value}}...{{end}}— sets.to.Valueinside the block; skips if nil/zero{{.FieldName}}— accesses the named field of the current dot value{{$var := .Value}}— assigns to a local variable
Template Caching
Parsing templates is slow — it reads files and compiles them. Parse once at startup, reuse on every request:
var templates *template.Template
func init() {
// template.Must panics at startup if templates fail to parse
// This is appropriate — a broken template is a deployment error, not a runtime error
templates = template.Must(template.ParseGlob("templates/*.html"))
}
func renderTemplate(w http.ResponseWriter, name string, data any) {
if err := templates.ExecuteTemplate(w, name, data); err != nil {
// At this point, the response header may already be sent
// Log the error but we can't change the status code
slog.Error("template execution failed",
slog.String("template", name),
slog.Any("error", err))
}
}
// Usage in handler
func homePage(w http.ResponseWriter, r *http.Request) {
renderTemplate(w, "home.html", PageData{
Title: "Home",
User: currentUser(r),
})
}
In development, you may want to re-parse on every request to see changes immediately. Wrap it in a flag:
var devMode = os.Getenv("DEV_MODE") == "true"
func getTemplates() *template.Template {
if devMode {
return template.Must(template.ParseGlob("templates/*.html"))
}
return templates // cached
}
Custom Template Functions
The FuncMap adds custom functions callable from templates:
import (
"html/template"
"time"
)
func buildTemplates() *template.Template {
funcMap := template.FuncMap{
// Format a time.Time as a human-readable date
"formatDate": func(t time.Time) string {
return t.Format("January 2, 2006")
},
// Format cents (int) as dollars
"formatPrice": func(cents int) string {
return fmt.Sprintf("$%.2f", float64(cents)/100)
},
// Truncate a string with an ellipsis
"truncate": func(n int, s string) string {
if len(s) <= n {
return s
}
return s[:n] + "…"
},
// Mark a string as safe HTML (bypasses escaping — use carefully)
"safeHTML": func(s string) template.HTML {
return template.HTML(s)
},
}
return template.Must(
template.New("").Funcs(funcMap).ParseGlob("templates/*.html"),
)
}
Usage in templates:
<p>Posted on {{.CreatedAt | formatDate}}</p>
<p>Price: {{.PriceCents | formatPrice}}</p>
<p>{{.Description | truncate 150}}</p>
safeHTML is an escape hatch — only use it for trusted HTML that has already been sanitized. Using it on user-provided content reintroduces XSS.
Template Composition: define and block
Large applications need shared layouts (header, nav, footer) with per-page content. Use {{define}} to name template blocks and {{block}} / {{template}} to compose them:
<!-- templates/layout.html -->
{{define "layout"}}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{{block "title" .}}My Site{{end}}</title>
<link rel="stylesheet" href="/static/css/main.css">
</head>
<body>
{{template "nav" .}}
<main>{{block "content" .}}{{end}}</main>
{{template "footer" .}}
</body>
</html>
{{end}}
{{define "nav"}}
<nav>
<a href="/">Home</a>
{{if .IsLoggedIn}}<a href="/logout">Log out</a>{{else}}<a href="/login">Log in</a>{{end}}
</nav>
{{end}}
{{define "footer"}}
<footer><p>© 2026 My Site</p></footer>
{{end}}
<!-- templates/home.html -->
{{template "layout" .}}
{{define "title"}}Home — My Site{{end}}
{{define "content"}}
<h1>Welcome, {{.Username}}!</h1>
<ul>
{{range .Posts}}
<li><a href="/posts/{{.Slug}}">{{.Title}}</a> — {{.CreatedAt | formatDate}}</li>
{{end}}
</ul>
{{end}}
func homePage(w http.ResponseWriter, r *http.Request) {
// templates.ExecuteTemplate starts with the named template
if err := templates.ExecuteTemplate(w, "layout", HomePageData{
Username: currentUser(r).Name,
IsLoggedIn: true,
Posts: getRecentPosts(),
}); err != nil {
slog.Error("template error", slog.Any("error", err))
}
}
{{block "title" .}}My Site{{end}} defines a block with a default value. If a child template defines {{define "title"}}...{{end}}, that overrides the default. If not, the default is used.
Preventing XSS
html/template prevents XSS for standard cases automatically. Two things can reintroduce it:
-
Using
template.HTML,template.URL, ortemplate.JS— these types bypass escaping. Only use them for values you’ve sanitized yourself. -
Inserting user content into JavaScript blocks —
html/templateescapes for JavaScript context, but if you’re dynamically generating JS logic (not just data), review carefully.
For user-generated content that should allow some HTML (rich text editors), use a whitelist-based HTML sanitizer before inserting into templates — github.com/microcosm-cc/bluemonday is the standard Go choice.
Summary
- Always use
html/template, nevertext/template, for HTML output — the escaping is automatic and context-aware http.FileServerwithhttp.Diris safe from path traversal — use it instead of serving files manually- Parse templates once at startup with
template.Must; re-parse in dev mode for instant feedback - Add custom functions via
FuncMapbefore callingParseGlob— functions must be registered before parsing {{define}}and{{block}}enable layout/page composition without repeating HTML structuretemplate.HTML(s)bypasses escaping — only use it for content you’ve sanitized through a trusted sanitizer
Resources
- html/template package documentation
- Go Blog: The Go template package
- bluemonday HTML sanitizer
- Go by Example: HTTP Servers
Comments