Golang - Template Tutorial
How to work with template files in Go.
Updated Mar 2, 2024

What you will learn

  • How to pre-parse templates from a directory.
  • How to pass arbitrary data to templates using a map.
  • How to use a base template.
  • How to pass default data to all templates.

Setup

Create main.go and add these lines to it:

package main

import (
	"fmt"
	"net/http"
	"log"
)

func homeHandler(
	w http.ResponseWriter,
	r *http.Request,
) {
	fmt.Fprintf(w, "Home page")
}

func aboutHandler(
	w http.ResponseWriter,
	r *http.Request,
) {
	fmt.Fprintf(w, "About page")
}

func main() {

	// Handlers.

	http.HandleFunc("/", homeHandler)
	http.HandleFunc("/about", aboutHandler)
	
	// Server.

	log.Printf("http://127.0.0.1:8080")
	err := http.ListenAndServe(":8080", nil)
	if err != nil {
		log.Fatalf("Server failed: %v", err)
	}
}

This will give you a basic app with two pages. Run these commands:

go mod init a
go build
a.exe or ./a.out

Parsing a single template

You can use the ParseFiles() function from the template package to parse one or more template files. Modify the homeHandler() function as follows:


import (
	"fmt"
	"net/http"
	"log"
	"html/template" // import this
)

func homeHandler(
	w http.ResponseWriter,
	r *http.Request,
) {
	tmpl, err := template.ParseFiles("templates/home.html")
	if err != nil {
		log.Printf("Error parsing home.html: %v",  err)
		http.Error(w, "Internal Server Error", http.StatusInternalServerError)
	}

	err = tmpl.Execute(w, nil)
	if err != nil {
		log.Printf("Template execution failed: %v", err)
		http.Error(w, "Internal Server Error", http.StatusInternalServerError)
	}
}
  • ParseFiles() returns a pointer to a Template struct that we can "execute" later to produce the final output.
  • The Execute() function accepts an output writer and data that you want to pass to the template.

Create templates/home.html and add this line to it:

<h1>Home page</h1>

Compile and run the program. Visit the home page, and you should see the "Home page" heading.

Passing data to templates

A simple way to pass data to the template is by using a map:

err = tmpl.Execute(w, map[string]interface{}{
	"Title": "Home",
	"Name": "John",
})

Update templates/home.html:

<h1>Hello, {{.Name}}.</h1>

The dot (.) refers to the current data context. You should see Hello, John. on the home page.

I like the flexibility of a map, but you might want to enforce stricter typing by using a struct for the passed data:

type PageData struct {
	Title string
	Name  string
}

data := PageData{
	Title: "Home",
	Name:  "John",
}
err := tmpl.Execute(w, data)

Base template

The "Base" template contains elements common to all pages. You can parse it with the child template like this:

tmpl, err := template.ParseFiles(
	"templates/base.html",
	"templates/home.html",
)

Create templates/base.html and specify a "content" block that child templates can fill in:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{{.Title}}</title>
</head>
<body>
    {{block "content" .}}{{end}}
</body>
</html>

The Title variable is passed to the base template in the Execute() function's data argument in homeHandler(). The dot (.) in {{block "content" .}}{{end}} indicates that the "content" block should inherit the current data context.

Edit templates/home.html and define the "content" block:

{{define "content"}}
<h1>Hello, {{.Name}}.</h1>
{{end}}

Visit the home page, and you should see this markup:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Home</title>
</head>
<body>
    
<h1>Hello, John.</h1>

</body>
</html>

Pre-parsing templates

At the moment, we are parsing the template files every time the homeHandler() function is called, that is, every time we visit the homepage.

Let's create a global templates map that stores pointers to parsed templates and add a function that parses these files from a templates directory when the app executable is run.

Make these changes:

// Import these:
import (
	"os"
	"path/filepath"
	// ...
)

// Globals.

// Add this map:
var templates = map[string]*template.Template{} 

// Add this function:
func parseTemplates() {
	files, err := os.ReadDir("templates")
	if err != nil {
		log.Fatalf("Error reading dir 'templates': %v", err)
	}
	for _, file := range files {
		if file.Type().IsRegular() {
			fileName := file.Name()
			// Ignore base template and Emacs temp files.
			if fileName == "base.html" || fileName[len(fileName)-1:] == "~" {
				continue
			}
			tmpl, err := template.ParseFiles(
				"templates/base.html",
				"templates/" + fileName,
			)
			if err != nil {
				log.Fatalf("Error parsing template %v: %v", fileName, err)
			}
			key := fileName[:len(fileName)-len(filepath.Ext(fileName))]
			templates[key] = tmpl 
		}
	}
}
  • The IsRegular() function ensures that we are parsing regular files, that is, files that are not directories or symbolic links, for example.
  • key := fileName[:len(fileName)-len(filepath.Ext(fileName))] extracts the base part of the filename.

Call the parseTemplates() function in the main() function:

func main() {

	// Init.

	parseTemplates() // here

	// Handlers.

	http.HandleFunc("/", homeHandler)
	http.HandleFunc("/about", aboutHandler)

	// ...

Update the homeHandler() function:


// Remove these lines:

/* tmpl, err := template.ParseFiles(
	"templates/base.html",
	"templates/home.html",
)
if err != nil {
	log.Fatalf("Error parsing home.html template: %v",  err)
}
*/	

// Use the template from the templates map:
err := templates["home"].Execute(w, map[string]interface{}{
	"Title": "Home",
	"Name": "John",
})

Specifying default template data

We can create a map that holds default data and merge it with the data passed to the template. Make the following changes:

import (
	"github.com/google/uuid" // import this
	// ..
)

// Globals.

var templates = map[string]*template.Template{}
// Add this:
var defaultTemplateData = map[string]interface{}{
	"Env": "Development",
	"CSSVersion": uuid.New().String(),
}

The CSSVersion variable can be used like this to prevent the browser from serving stale stylesheet data to the client:

<link href="/static/css/styles.css?v={{.CSSVersion}}" rel="stylesheet">

Run this:

go mod tidy

Add the following function:

// Merge defaultTemplateData and execute template.
func executeTemplate(
	w http.ResponseWriter,
	tmpl string,
	data map[string]interface{},
) error {

	// mergedData is at least the size of defaultTemplateData.
	mergedData := make(map[string]interface{}, len(defaultTemplateData))
	
	// Fill mergedData with the default data.
	for key, value := range defaultTemplateData {
		mergedData[key] = value
	}

	// Merge the data passed in the handler with the default data,
	// potentially overriding the default data.
	for key, value := range data {
		mergedData[key] = value
	}

	// Check if a given key exists in the templates map,
	// and execute the template with the mergedData.
	_, exists := templates[tmpl]
	if exists {
		err := templates[tmpl].Execute(w, mergedData)
		if err != nil {
			return err
		}
	} else {
		return fmt.Errorf("Key '%v' doesn't exist in templates.", tmpl)
	}
	
	return nil
}

Update homeHandler():

/*
err := templates["home"].Execute(w, map[string]interface{}{
	"Title": "Home",
	"Name": "John",
})
*/

err := executeTemplate(w, "home", map[string]interface{} {
	"Title": "Home",
	"Name": "John",
})

Update base.html:

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<title>{{.Title}}</title>
	<link href="/static/css/styles.css?v={{.CSSVersion}}" rel="stylesheet">
</head>
<body>
	{{block "content" .}}{{end}}
	<p>Env: {{.Env}}</p>
</body>
</html>

Visit the home page and you should see something like this:

Hello, John.
Env: Development

Final files

main.go

package main

import (
	"os"
	"path/filepath"
	"fmt"
	"net/http"
	"log"
	"html/template"
	"github.com/google/uuid"
)

// Globals.

var templates = map[string]*template.Template{}
var defaultTemplateData = map[string]interface{}{
	"Env": "Development",
	"CSSVersion": uuid.New().String(),
}

// Parse files in the "templates" folder.
func parseTemplates() {
	files, err := os.ReadDir("templates")
	if err != nil {
		log.Fatalf("Error reading dir 'templates': %v", err)
	}
	for _, file := range files {
		if file.Type().IsRegular() {
			fileName := file.Name()
			// Ignore base template and Emacs temp files.
			if fileName == "base.html" || fileName[len(fileName)-1:] == "~" {
				continue
			}
			tmpl, err := template.ParseFiles(
				"templates/base.html",
				"templates/" + fileName,
			)
			if err != nil {
				log.Fatalf("Error parsing template %v: %v", fileName, err)
			}
			key := fileName[:len(fileName)-len(filepath.Ext(fileName))]
			templates[key] = tmpl 
		}
	}
}

// Merge defaultTemplateData and execute template.
func executeTemplate(
	w http.ResponseWriter,
	tmpl string,
	data map[string]interface{},
) error {

	mergedData := make(map[string]interface{}, len(defaultTemplateData))
	
	for key, value := range defaultTemplateData {
		mergedData[key] = value
	}

	for key, value := range data {
		mergedData[key] = value
	}

	_, exists := templates[tmpl]
	if exists {
		err := templates[tmpl].Execute(w, mergedData)
		if err != nil {
			return err
		}
	} else {
		return fmt.Errorf("Key '%v' doesn't exist in templates.", tmpl)
	}
	
	return nil
}

func homeHandler(
	w http.ResponseWriter,
	r *http.Request,
) {
	/*
	tmpl, err := template.ParseFiles(
		"templates/base.html",
		"templates/home.html",
	)
	if err != nil {
		log.Printf("Error parsing home.html: %v",  err)
		http.Error(w, "Internal Server Error", http.StatusInternalServerError)
	}

	err := templates["home"].Execute(w, map[string]interface{}{
		"Title": "Home",
		"Name": "John",
	})
	*/
	   
	err := executeTemplate(w, "home", map[string]interface{} {
		"Title": "Home",
		"Name": "John",
	})
	
	if err != nil {
		log.Printf("Template execution failed: %v", err)
		http.Error(w, "Internal Server Error", http.StatusInternalServerError)
	}
}

func aboutHandler(
	w http.ResponseWriter,
	r *http.Request,
) {
	fmt.Fprintf(w, "About page")
}

func main() {

	// Init.

	parseTemplates()
	
	// Handlers.

	http.HandleFunc("/", homeHandler)
	http.HandleFunc("/about", aboutHandler)
	
	// Server.

	log.Printf("http://127.0.0.1:8080")
	err := http.ListenAndServe(":8080", nil)
	if err != nil {
		log.Fatalf("Server failed: %v", err)
	}
}

base.html

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<title>{{.Title}}</title>
	<link href="/static/css/styles.css?v={{.CSSVersion}}" rel="stylesheet">
</head>
<body>
	{{block "content" .}}{{end}}
	<p>Env: {{.Env}}</p>
</body>
</html>

home.html

{{define "content"}}
<h1>Hello, {{.Name}}.</h1>
{{end}}

Video