Go
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions and Methods
-
Section 4: Concurrency
-
Section 5: Packages and Tooling
-
Section 6: More Standard Library
-
Section 7: Building Services
-
Section 8: Advanced Go
-
Section 9: Go in the Cloud-Native Ecosystem
-
Section 10: Data Structures and Algorithms in Go
-
Section 11: Testing and Deployment
-
Section 12: Practical Projects
-
Section 13: More Standard Library Practice
-
Section 14: More Practice Projects
-
Section 15: Design Patterns in Go
-
Section 16: Interview Practice
-
Section 17: Package fmt In Depth
-
Section 18: Package strings and strconv
-
Section 19: Package os and io
-
Section 20: Package time
-
Section 21: Package sort and container
-
Section 22: Package encoding
-
Section 23: Package net/http In Depth
-
Section 24: Package context
-
Section 25: Package regexp and bytes
-
Section 26: Package errors In Depth
-
Section 27: Package crypto and hash
-
Section 28: Package flag and log
-
Section 29: Package sync In Depth
-
Section 30: More Practice Exercises
-
Section 31: Go Modules and Workspaces In Depth
-
Section 32: Generics Deep Dive (Go 1.18+)
-
Section 33: Testing Package In Depth
-
Section 34: More Interview and Whiteboard Practice
-
Section 35: Package math and unicode
-
Section 36: Package path and filepath
-
Section 37: Package database/sql
-
Section 38: Package text/template and html/template
-
Section 39: Package archive and compress
-
Section 40: Lower-Level net Package
-
Section 41: Package runtime and reflect
-
Section 42: Package embed and io/fs
-
Section 43: Go Assembly and CGO Basics
-
Section 44: Building CLIs and TUIs
-
Section 45: Go Performance Tuning
-
Section 46: More Real-World Projects
-
Section 47: Go in Production
-
Section 48: Go Design Patterns
-
Section 49: Go Interfaces Deep Dive
-
Section 50: Final Practice and Review
196: Template Functions and Pipelines
I've seen this happen to almost every dev moving from simple HTML templates to more complex logic in Go. You want to do something simple—like formatting a date or truncating a long string—so you write a helper function, add it to your FuncMap, and hit run. Then, the whole thing crashes with a "function not defined" error.
package main
import (
"html/template"
"os"
)
func truncate(s string) string {
if len(s) > 20 {
return s[:17] + "..."
}
return s
}
func main() {
funcMap := template.FuncMap{
"truncate": truncate,
}
// This looks correct, right?
tmpl, _ := template.New("web").Parse(`{{ .Body | truncate }}`)
tmpl.Funcs(funcMap) // Adding the functions here
tmpl.Execute(os.Stdout, map[string]string{"Body": "This is a very long piece of text that should be truncated."})
}
The "Function Not Defined" Panic
If you run the code above, it fails. You'll see a panic or an error stating that the function truncate is not defined. At first glance, it feels like a lie. You clearly defined truncate in your FuncMap and you called tmpl.Funcs(funcMap). So why does the template engine claim it doesn't exist?
The issue is the order of operations. In Go, the Parse method doesn't just store the string for later; it actually parses the template logic immediately to ensure the syntax is valid. When Parse hits {{ .Body | truncate }}, it looks at the current FuncMap to see if truncate is a known entity. Because we called Parse before Funcs, the map was empty during the parsing phase. The template engine fails the build before it ever gets to the execution phase.
Registering Before Parsing
The fix is deceptively simple: you must register your functions before you parse the template string or file. I usually wrap this in a helper variable to keep the chain clean.
func main() {
funcMap := template.FuncMap{
"truncate": truncate,
}
// Fix: New -> Funcs -> Parse
tmpl, err := template.New("web").Funcs(funcMap).Parse(`<div>{{ .Body | truncate }}</div>`)
if err != nil {
panic(err)
}
tmpl.Execute(os.Stdout, map[string]string{"Body": "This is a very long piece of text that should be truncated."})
}
By chaining .Funcs(funcMap) before .Parse(...), the parser now has a dictionary to reference. Now, when it sees truncate, it says, "Okay, I don't know how to run this yet, but I know it exists and it's valid," and it lets the program proceed.
Chaining Logic with Pipelines
Now that we have functions working, let's talk about the | symbol. In Go templates, this is called a pipeline. It works exactly like a Unix pipe: the output of the expression on the left becomes the final argument of the function on the right.
Imagine you want to clean up a user's bio: you want to trim the whitespace, convert it to lowercase, and then truncate it. Instead of nesting functions like a nightmare of parentheses—{{ truncate (toLower (trim .Bio)) }}—you use a pipeline:
{{ .Bio | trim | toLower | truncate }}
I find this much more readable. The data flows from left to right. One thing to keep in mind: if your function requires multiple arguments, the piped value is always added as the last argument. So if you have a function substring(start, end, text), you'd call it as {{ .Content | substring 0 10 }}. The .Content is passed in as the third argument, not the first.
📋 Practical Task
Build a Custom Product Price Formatter
You are building a product listing page. You need to create a template that takes a raw float64 price and processes it through a pipeline to make it "customer-ready."
Requirements:
- Create a
FuncMapwith two functions:addCurrency: Takes a string and prepends "$".roundPrice: Takes a float64 and returns it as a string formatted to two decimal places (usefmt.Sprintf).
- Set up a template that uses a pipeline to first round the price and then add the currency symbol (e.g.,
{{ .Price | roundPrice | addCurrency }}). - Ensure the functions are registered before the template is parsed to avoid the panic discussed in the lesson.
- Execute the template with a price like
123.4567, which should result in"$123.46".
There are no comments for now.