The copy-paste trap
You are building a Go web server. You have a list of users, a dashboard, and a settings page. Every page has the same navigation bar at the top and a copyright footer at the bottom. You copy-paste the HTML for the nav bar into every template file. Two weeks later, you need to add a "Logout" link. You find the nav bar in twelve different files and update each one. You miss one. The site breaks.
This is the problem partial templates solve. Go's html/template package lets you define reusable chunks of HTML once and drop them into other templates. You define the header in one file, the footer in another, and the layout in a third. The layout tells the engine where to inject the header and footer. You change the header in one place, and every page updates automatically.
How partials work
Go treats templates as a collection of named building blocks. You can define a block once and reference it by name from other blocks. The block is a partial. The main template is the layout. The layout says, "Put the header here, put the content here, put the footer here." The engine stitches them together at render time.
The key mechanism is template.ParseFiles. It reads multiple files and registers them in a single template set. Each file gets a name based on its filename. You can then reference one file from another using the {{template "filename"}} action inside the HTML. The name must match the filename exactly.
Here is the simplest setup. Three files. The layout includes the header and footer. The main program parses all three and executes the layout.
package main
import (
"html/template"
"os"
)
func main() {
// ParseFiles reads all listed files and registers them in one template set.
// Each file becomes available by its base filename.
tmpl, err := template.ParseFiles("layout.html", "header.html", "footer.html")
if err != nil {
panic(err)
}
// ExecuteTemplate picks the root template to render.
// The second argument is the name of the template to start from.
err = tmpl.ExecuteTemplate(os.Stdout, "layout.html", nil)
if err != nil {
panic(err)
}
}
The layout file uses the {{template}} action to inject the partials. The filename must match exactly what was passed to ParseFiles.
<!-- layout.html -->
<!DOCTYPE html>
<html>
<body>
<!-- template action looks up the name in the parsed set -->
{{template "header.html"}}
<h1>Main Content</h1>
{{template "footer.html"}}
</body>
</html>
The partials are just plain HTML snippets. They don't need to be valid documents.
<!-- header.html -->
<header>
<nav>Home | About</nav>
</header>
Footer follows the same pattern.
<!-- footer.html -->
<footer>
<p>Copyright 2024</p>
</footer>
ParseFiles uses the base name of the file as the template name. If you pass views/header.html, the name is header.html. The path doesn't become part of the name. This trips up people who expect the full path to be the key.
What happens at runtime
When you call ParseFiles, Go reads the files and builds an internal map of names to parsed ASTs. It doesn't render anything yet. ExecuteTemplate finds the AST for layout.html. It walks the tree. When it hits {{template "header.html"}}, it pauses the current execution, looks up header.html in the map, renders that, and resumes. The output streams to os.Stdout in order.
If a template name is missing, the engine panics at runtime. The compiler won't catch a typo in a template string. Template names are resolved dynamically. If you type {{template "headr.html"}}, the program compiles fine. At runtime, the template engine panics with template: "headr.html" is undefined. This happens because template names are strings resolved at render time, not compile time.
Parsing templates involves reading files, tokenizing, and building an AST. This takes milliseconds. Doing this inside an HTTP handler means every request pays the cost. Parse once. Execute many times. The server starts faster than the render.
Passing data to partials
Real apps pass data. The layout might need the current user, and the header might need the navigation links. You can pass a specific value to a partial by appending it to the template action.
Here is a handler that renders the layout with a user struct. The layout passes the user to the header.
package main
import (
"html/template"
"net/http"
)
// User represents a logged-in user for the demo.
type User struct {
Name string
}
// tmpl is the global template set.
// Must panics if parsing fails, which stops the server immediately.
// This is safe because templates are static assets loaded at startup.
var tmpl = template.Must(template.ParseFiles("layout.html", "header.html"))
func main() {
// Define a handler that renders the layout.
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// Create data for the page.
user := User{Name: "Alice"}
// ExecuteTemplate renders the layout.
// The third argument is the data available to the layout.
err := tmpl.ExecuteTemplate(w, "layout.html", user)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
})
// Listen on port 8080.
http.ListenAndServe(":8080", nil)
}
The layout receives the User struct. It can access {{.Name}}. To pass data to a partial, use {{template "name" value}}.
<!-- layout.html -->
<!DOCTYPE html>
<html>
<body>
<!-- Pass the current data context to the header -->
{{template "header.html" .}}
<p>Welcome, {{.Name}}</p>
</body>
</html>
The header can now use the data passed to it.
<!-- header.html -->
<header>
<span>Hi, {{.Name}}</span>
</header>
Partials don't automatically get the parent's data. You must pass it. If you don't, the partial receives nil. Accessing {{.Name}} in the partial causes a runtime error: template: "header.html": ...: can't evaluate field Name in type interface {}. Always pass the context explicitly if the partial needs data.
The Must convention
In production code, you rarely check errors from ParseFiles in main. You parse once at startup. The community convention is to use template.Must.
template.Must takes a *template.Template and an error. If the error is non-nil, it panics. If the error is nil, it returns the template. This lets you initialize templates at package level with a single line.
var tmpl = template.Must(template.ParseFiles("layout.html", "header.html"))
This is safe because templates are static assets. If parsing fails, the server shouldn't start. Panicking stops the process immediately, which is the correct behavior for a configuration error. You don't want a server running with broken templates.
gofmt doesn't touch HTML. But html/template has its own formatting quirks. Whitespace inside template actions is preserved. {{ .Name }} renders with spaces. {{.Name}} renders without spaces. Be careful with indentation in your HTML. The template engine doesn't strip whitespace automatically.
Pitfalls and silent bugs
Template errors hide until runtime. Test your templates with real data early.
If you pass header.html twice to ParseFiles, the second one overwrites the first. The compiler doesn't warn. The runtime uses the last one. This breaks layouts silently. If you have views/header.html and partials/header.html, and both are passed, the last one wins. Check your file lists carefully.
ParseFiles requires all files to exist. If one is missing, the call fails immediately with a file-not-found error. You can't lazily load templates with ParseFiles. If you need dynamic loading, use ParseGlob or Parse, but those come with their own risks.
html/template auto-escapes HTML entities. Partials inherit this. If you inject a partial, it's safe. If you use template.HTML, you bypass safety. Never use template.HTML for user input. Partials are safe by default. Trust the engine.
The worst goroutine bug is the one that never logs. The worst template bug is the one that renders silently wrong data. Validate your templates in tests.
When to use what
Use template.ParseFiles when you have a fixed set of template files and want explicit control over which files are loaded.
Use template.ParseGlob when you have many templates in a directory and want to load them all with a pattern like templates/*.html.
Use template.Must when you are initializing templates at package level and want a panic on failure rather than error handling in main.
Use {{define}} blocks inside a single file when your partials are small and you prefer keeping related templates together for portability.
Use ExecuteTemplate when you need to render a specific template from a set, rather than the default one.
Use Execute when you have a single template and don't need to select a name.
ParseFiles gives you clarity. Glob gives you convenience. Pick the one that matches your project structure.