How to Use html/template in Go

Web
Use html/template to parse a template string and execute it with data to generate safe HTML output.

How to Use html/template in Go

You have a user profile. You want to display the name. You write fmt.Sprintf("<h1>%s</h1>", name). It works until a user named <script>alert('xss')</script> signs up. Suddenly your app executes malicious code in every visitor's browser. String concatenation for HTML is a trap. Go provides html/template to solve this. The package renders HTML safely by tracking context and escaping values automatically.

Context-aware escaping

Think of a template as a stencil. You define the structure once. The template engine knows the context. If a blank is inside a <script> tag, it escapes quotes. If it's inside a href attribute, it escapes spaces. If it's in the body, it escapes < and >. The engine tracks where every value lands and applies the right escaping rules automatically. This prevents cross-site scripting attacks without you writing manual sanitization code.

The engine maintains a detailed state machine as it renders the output. It tracks the nesting level of tags. It knows if it is inside a <script> tag, a <style> tag, a tag attribute, or the document body. Each context requires different escaping rules. For example, inside a script, the engine escapes forward slashes to prevent breaking out of strings. Inside a URL attribute, it escapes spaces and quotes. Inside the body, it escapes angle brackets. You do not need to remember these rules. The engine applies them automatically based on the current position in the HTML structure.

The engine tracks context. You track data.

Minimal example

Here's the simplest template: parse a string, execute with data, write to output.

package main

import (
	"html/template"
	"os"
)

func main() {
	// Parse creates a template object. New sets the name for error reporting.
	// The name helps identify which template failed if you have many.
	tmpl, err := template.New("greeting").Parse("Hello, {{.}}!")
	if err != nil {
		// Parse errors happen at startup. Panic is acceptable here for simple apps.
		// In production, handle this gracefully or use template.Must.
		panic(err)
	}

	// Execute writes the rendered output to os.Stdout.
	// The dot (.) represents the root data passed to the template.
	err = tmpl.Execute(os.Stdout, "World")
	if err != nil {
		panic(err)
	}
}

Walk through what happens

When you call Parse, the engine reads the string and builds an internal tree of nodes. It identifies actions like {{.}} and static text. It does not execute anything yet. The parsing phase validates syntax. If you have a mismatched brace, the parser catches it immediately. The parser also analyzes the structure to prepare for context-aware escaping.

When you call Execute, the engine walks the tree. It evaluates the actions against the data. It writes the static text directly to the output stream. For dynamic values, it checks the current context. If the context is HTML body, it escapes special characters. The result flows to the io.Writer you provided. The engine handles the heavy lifting of tracking context boundaries.

Parse builds the map. Execute walks the path.

Realistic example

Real apps use structs. Real apps serve HTTP. Structs hold the data. Templates hold the shape.

package main

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

// User represents a person in the system.
// Exported fields are accessible in the template.
type User struct {
	Name string
	Bio  string
}

// parseTemplate returns a compiled template or panics on error.
// This pattern is common for static templates loaded at startup.
func parseTemplate() *template.Template {
	// ParseFiles or Parse can load multiple templates.
	// Here we parse a single string for demonstration.
	tmpl, err := template.New("profile").Parse(`
		<html>
		<body>
			<h1>{{.Name}}</h1>
			<p>{{.Bio}}</p>
		</body>
		</html>
	`)
	if err != nil {
		panic(err)
	}
	return tmpl
}
// handler serves the profile page.
func handler(w http.ResponseWriter, r *http.Request) {
	// Data to inject into the template.
	data := User{
		Name: "Alice",
		// Bio contains HTML tags. The template will escape them.
		Bio: "Loves <b>coding</b> and & coffee",
	}

	// Execute writes to the response writer.
	// The engine escapes <b> to &lt;b&gt; in the output.
	err := tmpl.Execute(w, data)
	if err != nil {
		http.Error(w, "Render error", http.StatusInternalServerError)
	}
}

func main() {
	tmpl := parseTemplate()
	http.HandleFunc("/profile", handler)
	http.ListenAndServe(":8080", nil)
}

The if err != nil check after Execute is verbose by design. The community accepts the boilerplate because it makes the unhappy path visible. Always check errors from template execution. A render failure should not crash the server. It should return a 500 error to the client.

Template actions

Templates support control flow. {{if .Condition}} checks truthiness. {{range .Items}} iterates slices. {{with .Field}} sets the dot to the field if it exists. These actions let you build dynamic pages without complex logic in the handler. The logic stays in the data preparation. The template handles presentation.

Go favors explicit control. The template syntax is minimal. It doesn't have complex logic operators. You can't do math or string manipulation inside the template easily. You prepare the data in Go code. The template just displays it. This separation keeps templates simple and testable. If you find yourself writing complex logic in a template, move it to the handler.

Pitfalls and errors

Templates are strings. The compiler doesn't check field names. If you misspell a field, the error appears at runtime. You get template: name:1: executing "name" at <.Field>: can't evaluate field Field in type main.User. This error tells you the template name, the line number, and the missing field. Check the line number and field name.

If you have a syntax error in the template, the parser rejects it with template: name:1: unexpected "}" in operand. Check the brace count. Templates use {{ and }}. A single } breaks the parser.

The template.HTML type allows you to bypass escaping. If you wrap user input in template.HTML, you take on the security burden. The engine trusts you. If you pass user input as template.HTML, you reintroduce XSS. Only use template.HTML for content you control completely, like static HTML snippets from your own assets. Trust the escaping. Don't bypass it unless you own the input.

Parsing is expensive. The engine builds a tree and analyzes context. Don't parse inside a request handler. Parse once at startup. If you do parse per request, your server will choke under load. Use template.Must for static templates to assert success at startup. The template.Must function is a community convention. It panics if the error is not nil. It reduces boilerplate for startup code where a parse error means the app cannot start.

Using text/template for HTML is a common mistake. text/template performs no automatic escaping. If you use it for HTML, you must escape everything manually. The compiler won't stop you. Choose the right package. html/template is for HTML. text/template is for other text.

Decision matrix

Use html/template when rendering HTML responses for browsers. The engine escapes values based on context to prevent XSS.

Use text/template when generating non-HTML output like emails, configuration files, or SQL queries. It performs no automatic escaping.

Use string concatenation or fmt.Sprintf only for simple, static strings where no user input is involved. Never use it for dynamic HTML.

Use a third-party template engine like Handlebars or Mustache only when you need client-side rendering or specific features Go's standard library lacks. The standard library is usually sufficient.

Pick the right tool for the output format.

Where to go next