Go Template Rendering: Server-Side HTML
Server-side rendering (SSR) delivers fully-formed HTML to the browser, eliminating the JavaScript-heavy initialization dance that plagues single-page applications. Go's template packages excel at...
Key Insights
- Go’s
html/templatepackage provides automatic XSS protection through context-aware escaping, making it significantly safer than string concatenation or text templates for HTML generation - Template composition with
blockandtemplateactions enables DRY layouts where you define base structures once and override specific sections per page - Parsing templates once at startup and caching them dramatically improves performance compared to parsing on every request—use
template.Must()for fail-fast initialization
Why Server-Side Rendering with Go
Server-side rendering (SSR) delivers fully-formed HTML to the browser, eliminating the JavaScript-heavy initialization dance that plagues single-page applications. Go’s template packages excel at this: they’re fast, memory-efficient, and ship with the standard library.
Use Go templates when you need:
- Fast initial page loads with minimal client-side JavaScript
- SEO-friendly content that’s immediately indexable
- Simple, progressive enhancement workflows
- Lower client-side complexity and battery drain
The html/template package is your default choice for web applications. It’s identical to text/template in syntax but adds automatic escaping to prevent XSS attacks. Never use text/template for HTML—you’ll regret it when user input breaks your pages or injects malicious scripts.
package main
import (
"html/template"
"os"
)
func main() {
tmpl := template.Must(template.New("hello").Parse("<h1>Hello, {{.Name}}!</h1>"))
data := struct{ Name string }{Name: "World"}
tmpl.Execute(os.Stdout, data)
}
Template Syntax Fundamentals
Templates use {{ }} delimiters for actions. The dot . represents the current data context—initially the data you pass to Execute(), but it changes inside control structures.
Variables start with $ and persist across their scope. Pipelines pass data through functions using the | operator, similar to Unix pipes.
{{- $user := .CurrentUser -}}
<div class="profile">
{{if $user.IsAdmin}}
<span class="badge">Admin</span>
{{else if $user.IsModerator}}
<span class="badge">Moderator</span>
{{else}}
<span class="badge">User</span>
{{end}}
<h2>{{$user.Name}}</h2>
{{range $user.Posts}}
<article>
<h3>{{.Title}}</h3>
<p>{{.Excerpt}}</p>
</article>
{{else}}
<p>No posts yet.</p>
{{end}}
</div>
The {{- and -}} syntax trims whitespace before and after actions, keeping your HTML clean. The range action iterates over slices, arrays, and maps, changing the dot context to each element. The else clause handles empty collections.
Passing Data to Templates
Structure your data with Go structs. Templates access exported fields (capitalized) using dot notation. This approach provides type safety and clarity.
type PageData struct {
Title string
CurrentUser *User
Posts []Post
Meta map[string]string
}
type User struct {
ID int
Name string
IsAdmin bool
Posts []Post
}
type Post struct {
ID int
Title string
Excerpt string
Author *User
}
func handleIndex(w http.ResponseWriter, r *http.Request) {
data := PageData{
Title: "My Blog",
CurrentUser: &User{
ID: 1,
Name: "Alice",
IsAdmin: true,
},
Posts: []Post{
{ID: 1, Title: "First Post", Excerpt: "Getting started..."},
{ID: 2, Title: "Second Post", Excerpt: "Going deeper..."},
},
Meta: map[string]string{
"description": "A technical blog",
"keywords": "go,templates,web",
},
}
tmpl.Execute(w, data)
}
Nested data works naturally. Access {{.CurrentUser.Name}} or iterate with {{range .Posts}}{{.Title}}{{end}}. Maps use the same syntax: {{.Meta.description}}.
Template Composition and Reusability
Real applications need layouts. Define a base template with block actions, then override those blocks in child templates.
templates/base.html:
{{define "base"}}
<!DOCTYPE html>
<html>
<head>
<title>{{block "title" .}}Default Title{{end}}</title>
</head>
<body>
<nav>{{template "nav" .}}</nav>
<main>
{{block "content" .}}
<p>No content defined.</p>
{{end}}
</main>
<footer>{{template "footer" .}}</footer>
</body>
</html>
{{end}}
{{define "nav"}}
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
</ul>
{{end}}
{{define "footer"}}
<p>© 2024 My Site</p>
{{end}}
templates/index.html:
{{template "base" .}}
{{define "title"}}Home - My Site{{end}}
{{define "content"}}
<h1>Welcome</h1>
{{range .Posts}}
<article>
<h2>{{.Title}}</h2>
<p>{{.Excerpt}}</p>
</article>
{{end}}
{{end}}
Parse multiple files together:
var tmpl *template.Template
func init() {
tmpl = template.Must(template.ParseGlob("templates/*.html"))
}
func handleIndex(w http.ResponseWriter, r *http.Request) {
data := getPageData()
tmpl.ExecuteTemplate(w, "base", data)
}
The block action defines a default implementation that child templates can override. The template action includes another template by name, passing the current data context.
Custom Functions and Pipelines
Built-in functions like len, index, and printf cover basics, but custom functions unlock real power.
func init() {
funcMap := template.FuncMap{
"formatDate": func(t time.Time) string {
return t.Format("Jan 2, 2006")
},
"truncate": func(s string, length int) string {
if len(s) <= length {
return s
}
return s[:length] + "..."
},
"add": func(a, b int) int {
return a + b
},
}
tmpl = template.Must(
template.New("").Funcs(funcMap).ParseGlob("templates/*.html"),
)
}
Use functions in templates:
<time>{{.PublishedAt | formatDate}}</time>
<p>{{.Content | truncate 150}}</p>
<span>Page {{add .CurrentPage 1}} of {{.TotalPages}}</span>
Pipelines chain operations left-to-right. The result of each function becomes the last argument to the next. This is powerful for transforming data inline without cluttering your Go handlers.
Security and Best Practices
The html/template package automatically escapes content based on context. HTML entities, JavaScript strings, CSS values, and URLs each get appropriate escaping.
type Comment struct {
Author string
Content string
}
// In template:
// <div class="comment">
// <strong>{{.Author}}</strong>
// <p>{{.Content}}</p>
// </div>
If a user submits <script>alert('XSS')</script> as content, it renders as escaped text: <script>alert('XSS')</script>. The browser displays it harmlessly.
Context-aware escaping handles JavaScript too:
<script>
var username = "{{.Username}}";
var posts = {{.PostsJSON}};
</script>
This correctly escapes quotes in Username and expects PostsJSON to be pre-marshaled JSON. For JSON, use a custom function:
"toJSON": func(v interface{}) template.JS {
b, _ := json.Marshal(v)
return template.JS(b)
}
Performance: Parse templates once at startup, not per request. Use template.Must() to panic on parse errors during initialization—fail fast rather than serving broken pages.
var tmpl *template.Template
func init() {
tmpl = template.Must(template.ParseGlob("templates/*.html"))
}
For development, re-parse on each request to see changes without restarting. Use build tags to separate dev and production behavior.
Real-World Example: Blog Post Renderer
Here’s a complete blog server with routing, error handling, and template rendering:
package main
import (
"html/template"
"log"
"net/http"
"time"
)
type Post struct {
ID int
Title string
Content string
PublishedAt time.Time
}
type PageData struct {
Title string
Posts []Post
}
var tmpl *template.Template
func init() {
funcMap := template.FuncMap{
"formatDate": func(t time.Time) string {
return t.Format("January 2, 2006")
},
}
tmpl = template.Must(
template.New("").Funcs(funcMap).ParseGlob("templates/*.html"),
)
}
func handleIndex(w http.ResponseWriter, r *http.Request) {
posts := []Post{
{
ID: 1,
Title: "Go Templates are Powerful",
Content: "Server-side rendering with Go is fast and secure...",
PublishedAt: time.Now().AddDate(0, 0, -2),
},
{
ID: 2,
Title: "Building Web Apps in Go",
Content: "The standard library provides everything you need...",
PublishedAt: time.Now().AddDate(0, 0, -1),
},
}
data := PageData{
Title: "My Blog",
Posts: posts,
}
if err := tmpl.ExecuteTemplate(w, "base", data); err != nil {
log.Printf("Template error: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
}
func main() {
http.HandleFunc("/", handleIndex)
log.Println("Server starting on :8080")
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(err)
}
}
This pattern scales. Add more handlers, extract data fetching to a repository layer, and expand your templates. The fundamentals remain: parse once, execute per request, handle errors gracefully.
Go templates aren’t flashy, but they’re reliable, fast, and secure by default. For content-focused sites, admin panels, and server-rendered applications, they’re an excellent choice that keeps your stack simple and your users happy with fast page loads.