How to Use Template Functions (FuncMap) in Go

Web
Register custom Go functions in a template.FuncMap and pass them to template.New().Funcs() to use them inside your templates.

The missing piece in your template

You are building a small web dashboard. The backend pulls a list of transactions, and you want the template to display dates in a readable format and prices with a dollar sign. You write {{ .Date.Format "Jan 2" }} and it works. Then you try {{ .Price | formatCurrency }} and the template engine throws a fit. Go's standard library templates are deliberately strict. They refuse to execute arbitrary Go code to prevent security holes and keep rendering predictable. You cannot just import strings or time inside the template file. You need a bridge.

Teaching the engine new words

That bridge is template.FuncMap. It is a dictionary that maps a string name to a Go function. When the template engine encounters {{ formatCurrency .Price }}, it looks up formatCurrency in that dictionary, finds the corresponding Go function, and calls it with the provided arguments. Think of it like teaching a specialized assistant a handful of custom commands. The assistant does not know how to do everything, but you can hand it a cheat sheet before it starts working. The cheat sheet is the FuncMap.

The template engine has a restricted vocabulary by design. It ships with a small set of built-in functions like eq, printf, and index. Everything else requires explicit registration. This restriction keeps templates declarative and keeps business logic out of your HTML files. You define the function in Go, register it under a name, and call that name in the template. The separation stays clean.

The minimal setup

Here is the simplest way to register a custom function and use it in a template.

package main

import (
	"os"
	"strings"
	"text/template"
)

func main() {
	// Map a template keyword to a standard library function
	funcs := template.FuncMap{
		"upper": strings.ToUpper,
	}

	// Chain Funcs before Parse so the engine knows about "upper"
	tmpl := template.Must(template.New("t").Funcs(funcs).Parse("Hello {{upper \"world\"}}"))

	// Execute the template with no data context
	tmpl.Execute(os.Stdout, nil)
}

The Funcs method returns the template pointer, which enables method chaining. You register the map, parse the template string, and then execute it. The order matters. The template engine reads the function map during the parsing phase. If you try to parse a template that references upper before calling Funcs, the parser will reject it.

Register functions before you parse. Parse before you execute. The pipeline is strict.

How the engine resolves your functions

When you call Parse, the template engine tokenizes the string and builds an internal tree of nodes. Every time it sees a function call, it checks the registered FuncMap. If the name exists, it records the function pointer in the AST. If the name is missing, parsing fails immediately. This early validation saves you from runtime panics later.

During execution, the engine walks the tree. It evaluates arguments, passes them to the Go function, and inserts the result into the output stream. The function runs in the same goroutine as the template execution. There is no hidden concurrency. If your custom function blocks for ten seconds, the entire template render blocks for ten seconds.

The engine uses Go's reflect package to call your function dynamically. It does not know your function signature at compile time. It only knows the name and the number of arguments you pass in the template. This means you must be careful with types. The engine passes arguments exactly as they appear in the template. String literals arrive as string. Unquoted numbers arrive as float64. Booleans arrive as bool. If your function expects an int, the engine cannot convert it automatically. You get a runtime panic with reflect: Call of type int with float64 argument. Always match your function signatures to the template input, or use type assertions inside the function.

Your function can accept any number of arguments. The engine supports variadic functions. If you define func join(sep string, parts ...string) string, you can call it in the template as {{ join ", " .A .B .C }}. The engine packs the extra arguments into a slice automatically.

Your function must return either a single value or two values. If you return two values, the second must be an error. The engine checks the error. If it is non-nil, template execution stops and returns that error. This matches Go's standard error handling pattern. The community accepts the boilerplate because it makes failure paths visible. You can catch template rendering failures the same way you catch database errors.

Keep your template functions pure. They should not modify global state, write to files, or make network requests. Treat them as mathematical functions that take input and return output.

A realistic rendering pipeline

Real applications rarely need just one custom function. You usually need a small toolkit for formatting, math, or conditional logic. Here is how you build a reusable template set with multiple functions.

package main

import (
	"fmt"
	"strings"
	"text/template"
	"time"
)

// formatCurrency turns a float64 into a dollar string
func formatCurrency(amount float64) string {
	return fmt.Sprintf("$%.2f", amount)
}

// daysSince calculates the difference between two times
func daysSince(t time.Time) int {
	return int(time.Since(t).Hours() / 24)
}

// contains checks if a string includes a substring
func contains(s, substr string) bool {
	return strings.Contains(s, substr)
}

These functions live in your Go package. They are ordinary functions with ordinary signatures. The template engine does not care about their visibility. Public and private names start with capital and lowercase letters respectively. The template only sees the string key you register. You can name the template function anything you want, regardless of the Go function name.

Here is how you wire them together and render a report.

package main

import (
	"os"
	"text/template"
	"time"
)

func main() {
	// Bundle multiple helpers into one map
	funcs := template.FuncMap{
		"currency": formatCurrency,
		"daysAgo":  daysSince,
		"contains": contains,
	}

	// Register functions once, then parse the template string
	tmpl := template.Must(template.New("report").Funcs(funcs).Parse(`
	Report generated on {{ .Date.Format "Mon Jan 2" }}
	Status: {{ if contains .Status "active" }}Active{{ else }}Inactive{{ end }}
	Total: {{ currency .Amount }}
	Age: {{ daysAgo .Created }} days
	`))

	type Report struct {
		Date    time.Time
		Status  string
		Amount  float64
		Created time.Time
	}

	data := Report{
		Date:    time.Now(),
		Status:  "active_verified",
		Amount:  1250.5,
		Created: time.Now().AddDate(0, 0, -45),
	}

	// Render the template to standard output
	tmpl.Execute(os.Stdout, data)
}

The template engine treats every registered function as a black box. It only cares about the signature and the return value. You can reuse the same FuncMap across multiple templates. Create a base template with template.New("").Funcs(funcs) and clone it for each file using ParseFiles or ParseGlob. The cloned templates inherit the function map automatically. This keeps your registration logic in one place.

Trust the type system. Wrap the value or change the design.

Where things break

Custom template functions introduce a few common traps. The first is type mismatch. The template engine passes arguments exactly as they appear in the template. If you pass a string literal, it arrives as a string. If you pass a number without quotes, it arrives as a float64. If your function expects an int, the engine cannot convert it automatically. The runtime panics with reflect: Call of type int with float64 argument. Always match your function signatures to the template input, or use type assertions inside the function.

Another trap is forgetting to register the map before parsing. If you call Parse first and then chain Funcs, the parser has already scanned the template and rejected the unknown function name. The compiler does not catch this at build time. You get a runtime error from the template package: template: "t":1: function "upper" not defined. The fix is simple. Always chain Funcs before Parse, or create a base template with template.New("").Funcs(funcs) and clone it for each file.

You also need to watch out for HTML escaping. The html/template package automatically escapes output to prevent XSS attacks. If your custom function returns raw HTML, the engine will escape the angle brackets. To bypass this, return a template.HTML type instead of a plain string. The engine recognizes the type and skips escaping. This is a safety feature, not a bug. Trust the type system. Wrap the value or change the design.

Goroutine leaks happen when the goroutine waits on a channel that never gets closed. Always have a cancellation path. Template execution runs synchronously, so you do not need channels here. But if you spawn background workers to precompute template data, remember to wire up context.Context as the first parameter, conventionally named ctx. Functions that take a context should respect cancellation and deadlines.

The worst goroutine bug is the one that never logs. Keep your template functions fast and synchronous.

When to reach for custom functions

Use template.FuncMap when you need to keep formatting logic out of your Go structs and inside the presentation layer. Use pre-processing in Go when the calculation is expensive or requires database lookups. Use built-in template functions like printf, eq, or index when the standard library already covers your case. Use a third-party templating engine when you need advanced features like loops with indices, complex inheritance, or server-side rendering frameworks.

Goroutines are cheap. Channels are not magic. Template functions are just Go functions with a different calling convention.

Where to go next