Day 28: Embedding, Templates & Build Tags
Master three production-grade Go features in one day: baking files into your binary with //go:embed, generating text and HTML with text/template and html/template, and controlling what gets compiled with build tags. Every concept is introduced alongside a hands-on lab so you build and verify as you learn.
Table of Contents
- Section 1: //go:embed
- Section 2: text/template
- Section 3: html/template — Safe HTML Rendering
- Section 4: Build Tags
- Section 5: go:generate
- Final Project: Go Book Store
- Official Documentation
Section 1: //go:embed
//go:embed is a compile-time directive that instructs the Go toolchain to read one or more files or directories from the filesystem and bake their contents directly into the binary. The result is a fully self-contained executable — no config files, template directories, or static assets need to be shipped alongside it.
How it works
source tree at build time:
assets/
banner.txt ← read and encoded into the binary
index.html
style.css
After go build, the binary contains the file bytes. The originals are not needed at runtime.
Three embedding targets
| Declaration | Type | Best for |
|---|---|---|
//go:embed file.txt var f []byte |
[]byte |
single binary file |
//go:embed file.txt var s string |
string |
single text file |
//go:embed dir var e embed.FS |
embed.FS |
trees of files |
Rules and restrictions
- The path must be relative and must not contain
.. - Files whose names begin with
.or_are skipped by default — to include them, use theall:prefix://go:embed all:dir - The
embedpackage must be imported (even if only the blank identifier is used) when you use//go:embed - The directive must appear immediately before the
vardeclaration with no blank lines between them
import _ "embed" // blank import is enough when using []byte / string forms
//go:embed assets/banner.txt
var banner []byte
// With embed.FS you need the named import
import "embed"
//go:embed assets
var assets embed.FS
Reading embedded files
// []byte / string — use directly
fmt.Println(string(banner))
// embed.FS — use the io/fs helpers
data, err := fs.ReadFile(assets, "assets/index.html")
entries, err := fs.ReadDir(assets, "assets")
// fs.Sub strips a prefix, returning a plain fs.FS rooted at that directory
sub, err := fs.Sub(assets, "assets")
data, err = fs.ReadFile(sub, "index.html") // no "assets/" prefix needed
Lab 1: Embed a Single File as []byte
What you’ll practise: Declaring a //go:embed variable and verifying the binary is self-contained.
Task:
Create assets/banner.txt containing ASCII art. Embed it as a []byte variable and print it on startup. Build the binary and confirm it runs without the source tree.
Steps:
- Create
assets/banner.txtwith any ASCII art (several lines of text). - Add
import _ "embed"to your source file. - Declare
//go:embed assets/banner.txtimmediately abovevar banner []byte. - In
main(), callfmt.Println(string(banner)). - Run with
go run ., then build withgo build -o lab1and run./lab1from a different directory (copy the binary).
package main
import (
_ "embed"
"fmt"
)
//go:embed assets/banner.txt
var banner []byte
func main() {
fmt.Println(string(banner))
}
Expected output:
____ ____ ___
/ ___| ___ |___ \ / _ \
| | _ / _ \ __) || | | |
| |_| | (_) |/ __/ | |_| |
\____|\___/|_____| \___/
Day 28: Embedding, Templates & Build Tags
Checkpoint: Run go build -o lab1 . && ./lab1. The banner prints even when you delete assets/banner.txt after building.
Lab 2: Embed a Directory as embed.FS
What you’ll practise: Using embed.FS with fs.ReadFile and fs.ReadDir to list and read files from an embedded directory.
Task:
Add assets/script.js to the existing assets/ directory (which already has index.html and style.css). Embed the whole directory as embed.FS, then use fs.ReadDir to list every file and fs.ReadFile to print the first 40 bytes of each.
Steps:
- Create
assets/script.js— a few lines of JavaScript is fine. - Add
import "embed"andimport "io/fs". - Declare
//go:embed assetsimmediately abovevar assets embed.FS. - Use
fs.ReadDir(assets, "assets")to iterate entries. - Use
fs.ReadFile(assets, "assets/"+name)to read each file.
package main
import (
"embed"
"fmt"
"io/fs"
"log"
)
//go:embed assets
var assets embed.FS
func main() {
entries, err := fs.ReadDir(assets, "assets")
if err != nil {
log.Fatal(err)
}
for _, e := range entries {
info, _ := e.Info()
data, _ := fs.ReadFile(assets, "assets/"+e.Name())
preview := string(data)
if len(preview) > 40 {
preview = preview[:40] + "..."
}
fmt.Printf("%-20s %5d bytes %q\n", e.Name(), info.Size(), preview)
}
}
Expected output:
banner.txt 96 bytes " ____ ____ ___\n / ___| ..."
index.html 427 bytes "<!DOCTYPE html>\n<html lang=\"en\">\n..."
script.js 58 bytes "// Day 28: demo script\ndocument.ad..."
style.css 512 bytes "/* Day 28 stylesheet */\n\n*, *::be..."
Checkpoint: Run go run .. All four files appear. Delete any asset file — go run . still works because the files are embedded.
Lab 3: Serve Embedded Files over HTTP
What you’ll practise: Wiring embed.FS into http.FileServer to serve a directory with no runtime file reads.
Task:
Build a minimal HTTP server that serves everything in assets/ under the URL path /assets/ using the embedded filesystem from Lab 2.
Steps:
- Import
"net/http","embed", and"io/fs". - Use
fs.Sub(assets, "assets")to strip theassets/prefix from the embedded tree. - Wrap the result in
http.FileServer(http.FS(sub))and mount it at/assets/withhttp.StripPrefix. - Add a root handler that reads and serves
index.htmlfrom the embedded FS. - Listen on
:8080and test withcurl http://localhost:8080/assets/style.css.
sub, err := fs.Sub(assets, "assets")
if err != nil {
log.Fatal(err)
}
mux := http.NewServeMux()
mux.Handle("/assets/", http.StripPrefix("/assets/", http.FileServer(http.FS(sub))))
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
content, err := fs.ReadFile(sub, "index.html")
if err != nil {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write(content)
})
log.Println("listening on :8080")
log.Fatal(http.ListenAndServe(":8080", mux))
Expected output:
$ curl -s http://localhost:8080/assets/style.css | head -3
/* Day 28 stylesheet */
*, *::before, *::after {
Checkpoint: go run . starts the server. curl http://localhost:8080/ returns the HTML. curl http://localhost:8080/assets/style.css returns the stylesheet. curl http://localhost:8080/missing returns a 404. Kill the source files and rebuild — the binary still serves everything.
Section 2: text/template
The text/template package provides data-driven text generation. A template is a string (or file) containing static text interspersed with actions — expressions enclosed in `` — that are evaluated against a data value at execution time.
Terminology
| Term | Meaning |
|---|---|
| Template | The string containing static text and actions |
| Data | The Go value passed to Execute — called “dot” (.) inside the template |
| Action | Code inside `` — conditionals, loops, definitions, calls |
| Pipeline | A sequence of commands chained with \| |
Core actions
print dot (the current data value)
print a struct field or map key
call a method; method must return (T) or (T, error)
… conditional
… …
… iterate; dot becomes each element
… iterate map entries
… execute block with dot set to .Val (skipped if zero)
… define a named sub-template
include a named sub-template, passing dot
… define + execute (can be overridden)
Pipelines
pipe dot.Name into printf
custom function from FuncMap
Whitespace trimming
A dash inside a delimiter trims adjacent whitespace:
``` trim whitespace BEFORE this action trim whitespace AFTER this actiontrim both sides
### The API
```go
// Parse from a string
t := template.Must(template.New("name").Parse(`Hello, !`))
// Execute — writes to any io.Writer
t.Execute(os.Stdout, data)
// Execute a named sub-template
t.ExecuteTemplate(os.Stdout, "header", data)
// Parse from files
t, err := template.ParseFiles("tmpl/a.html", "tmpl/b.html")
// Parse from an fs.FS (works with embed.FS)
t, err := template.ParseFS(fsys, "tmpl/*.html")
template.Must
template.Must panics if the error is non-nil — useful for package-level initialization where a parse failure is a programming error:
var report = template.Must(template.New("report").Parse(`
-
`))
Lab 4: Hello Template
What you’ll practise: Parsing a template from a string and executing it against a struct with Execute.
Task:
Parse the string "Hello, ! You have messages.", execute it with a struct, and write the output to os.Stdout.
Steps:
- Define a struct with
Name stringandCount int. - Parse the template with
template.Must(template.New("hello").Parse(...)). - Call
t.Execute(os.Stdout, data)with an instance of your struct. - Try changing the data values and observe how the output changes.
- Replace
os.Stdoutwith astrings.Builder— capture the output and print its length.
package main
import (
"os"
"text/template"
)
type Inbox struct {
Name string
Count int
}
var tpl = template.Must(template.New("hello").Parse(
"Hello, ! You have messages.\n",
))
func main() {
tpl.Execute(os.Stdout, Inbox{Name: "Gopher", Count: 3})
tpl.Execute(os.Stdout, Inbox{Name: "Alice", Count: 0})
}
Expected output:
Hello, Gopher! You have 3 messages.
Hello, Alice! You have 0 messages.
Checkpoint: go run . produces exactly the two lines above with no extra whitespace. Swap the struct for a map[string]any{"Name": "Bob", "Count": 7} — the template executes identically.
Lab 5: Control Flow — range, if, and Whitespace Trimming
What you’ll practise: Using ,, ``, and whitespace-trimming dashes to generate a tidy text report.
Task:
Build a template that receives a []string of to-do items and renders a numbered plain-text report. Items whose text starts with "DONE:" should be marked complete; all others are pending. Show how adding `` cleans up unwanted blank lines.
Steps:
- Declare
type Report struct { Title string; Items []string }. - Write a template that ranges over
Items, uses `` to detect the"DONE:"prefix, and prints a status indicator. - Use
andto eliminate extra blank lines between iterations. - Execute with a mix of done and pending items.
- Compare the output with and without the
-trims.
const reportTpl = `=== ===
[x]
[ ]
Total: items
`
funcs := template.FuncMap{
"hasPrefix": strings.HasPrefix,
}
t := template.Must(template.New("report").Funcs(funcs).Parse(reportTpl))
t.Execute(os.Stdout, Report{
Title: "Sprint Tasks",
Items: []string{
"DONE: Write tests",
"Add documentation",
"DONE: Fix CI",
"Deploy to staging",
},
})
Expected output:
=== Sprint Tasks ===
[x] DONE: Write tests
[ ] Add documentation
[x] DONE: Fix CI
[ ] Deploy to staging
Total: 4 items
Checkpoint: go run . prints the report with no extra blank lines between items. Remove the - from and — extra blank lines appear. Restore them.
Lab 6: Named Templates and Composition
What you’ll practise: Defining reusable sub-templates with and including them with.
Task:
Build a multi-section text report by defining a "header" and a "footer" sub-template, then compose them into a "body" template using and. Parse all definitions together using a single template.Must(template.New("").Parse(...)) call on a concatenated string.
Steps:
- Write a `` block that prints a decorated title.
- Write a `` block that prints a separator and a timestamp field.
- Write a `` block that calls header, ranges over
.Sections, then calls footer. - Use
t.ExecuteTemplate(os.Stdout, "body", data)to render the composed output. - Try calling
t.ExecuteTemplate(os.Stdout, "header", data)on its own — observe that sub-templates are individually executable.
const (
headerTpl = `
+--- ---+
`
footerTpl = `
+--- Generated: ---+
`
bodyTpl = `
*
`
)
type Doc struct {
Title string
Sections []string
Generated string
}
t := template.Must(template.New("").Parse(headerTpl + footerTpl + bodyTpl))
t.ExecuteTemplate(os.Stdout, "body", Doc{
Title: "Weekly Report",
Sections: []string{"Q3 revenue up 12%", "New hire onboarded", "CI fixed"},
Generated: "2026-10-01",
})
Expected output:
+--- Weekly Report ---+
* Q3 revenue up 12%
* New hire onboarded
* CI fixed
+--- Generated: 2026-10-01 ---+
Checkpoint: go run . prints the composed output. Rename "body" to "report" and update the ExecuteTemplate call — confirm it still works.
Lab 7: FuncMap — Custom Template Functions
What you’ll practise: Registering custom Go functions via template.FuncMap and calling them inside a template with pipelines.
Task:
Register three functions — upper (wraps strings.ToUpper), join (wraps strings.Join), and now (returns a formatted timestamp string) — then use them in a template that renders an inventory report.
Steps:
- Build a
template.FuncMapwith"upper","join", and"now"keys. - Call
t.Funcs(funcMap)before callingt.Parse(...)— order matters. - In the template, use
,, and ``. - Verify that trying to call an unregistered function causes a parse error.
- Add a fourth function
"currency"that formats afloat64as"$XX.XX".
funcs := template.FuncMap{
"upper": strings.ToUpper,
"join": strings.Join,
"now": func() string { return time.Now().Format("2006-01-02") },
}
const tplStr = `Item :
Tags :
Date :
`
t := template.Must(template.New("item").Funcs(funcs).Parse(tplStr))
t.Execute(os.Stdout, struct {
Name string
Tags []string
}{"widget", []string{"hardware", "v2", "sale"}})
Expected output:
Item : WIDGET
Tags : hardware, v2, sale
Date : 2026-10-01
Checkpoint: go run . prints the three lines. Comment out the "upper" entry from FuncMap, run again — you get a parse error mentioning "upper" is undefined. Restore it.
Lab 8: Parse Templates from an Embedded fs.FS
What you’ll practise: Using template.ParseFS to load a template stored inside an embed.FS, combining the embedding knowledge from Labs 1–3 with the template knowledge from Labs 4–7.
Task:
Store a text report template in assets/report.tmpl. Parse it with template.ParseFS, passing the embedded FS from Lab 2. Execute it with a list of structs representing products.
Steps:
- Create
assets/report.tmplcontaining a template that ranges over a[]Productslice and prints each product’s name and price. - Import
"embed","io/fs", and"text/template". - Declare
//go:embed assetsand embed the directory. - Use
template.ParseFS(assets, "assets/report.tmpl")to parse. - Execute with a slice of product structs.
// assets/report.tmpl
Product Report
==============
Total: products
//go:embed assets
var assets embed.FS
type Product struct {
Name string
Price float64
}
func main() {
t, err := template.ParseFS(assets, "assets/report.tmpl")
if err != nil {
log.Fatal(err)
}
products := []Product{
{"The Go Programming Language", 49.99},
{"Learning Go, 2nd Edition", 39.99},
{"Concurrency in Go", 34.99},
}
t.Execute(os.Stdout, products)
}
Expected output:
Product Report
==============
The Go Programming Language $49.99
Learning Go, 2nd Edition $39.99
Concurrency in Go $34.99
Total: 3 products
Checkpoint: go run . renders the report. Delete assets/report.tmpl from disk and rebuild — go build fails (the file must be present at build time). This is the trade-off: file must exist when you build, not when you run.
Section 3: html/template — Safe HTML Rendering
html/template is a drop-in replacement for text/template with one critical difference: it automatically escapes all data values before inserting them into HTML output.
Why it matters
If a user provides the string <script>alert('xss')</script> and you insert it into HTML with text/template, it executes as JavaScript in the browser. html/template encodes it to <script>alert('xss')</script>, making it safe.
Same API, different package
import "html/template" // instead of "text/template"
// Everything else is identical: New, Parse, Execute, ParseFS, FuncMap, Must
t := template.Must(template.New("page").Parse(`<p></p>`))
t.Execute(os.Stdout, "<b>bold</b>")
// output: <p><b>bold</b></p>
Context-aware escaping
html/template understands HTML context — it applies different escaping rules depending on where data appears:
| Context | Example | Escaping applied |
|---|---|---|
| HTML content | <p></p> |
HTML entity encoding |
| Attribute value | <a href=""> |
URL + attribute encoding |
| JavaScript string | <script>var x=</script> |
JS string escaping |
| CSS value | <div style="color:"> |
CSS value escaping |
Trusted HTML with template.HTML
When you have HTML you’ve generated yourself and trust completely, wrap it in template.HTML to bypass escaping:
safe := template.HTML("<strong>trusted content</strong>")
t.Execute(w, safe) // rendered as-is, not escaped
Never use template.HTML with user input.
Rule of thumb
Always use html/template for any output that ends up in a browser. Use text/template only for plain text, config files, or code generation.
Lab 9: Auto-Escaping and XSS Prevention
What you’ll practise: Observing the concrete difference between text/template and html/template when rendering untrusted user input.
Task:
Render a user-supplied string containing <script>alert('xss')</script> with both packages and compare the output. This is the single most important reason to choose html/template over text/template for web output.
Steps:
- Write a helper
render(pkg string, tplStr string, data any)that parses the template using both packages and prints the result. - Render
<p></p>with the XSS payload usingtext/template. - Render the same template and data using
html/template. - Observe:
text/templatepasses the script tag through;html/templateescapes it. - Try injecting into an attribute:
<a href="">link</a>withURL: "javascript:alert(1)"— note howhtml/templatealso neutralises this.
import (
textTpl "text/template"
htmlTpl "html/template"
)
payload := `<script>alert('xss')</script>`
// text/template — DANGEROUS for HTML output
t1, _ := textTpl.New("").Parse("<p>User says: </p>")
fmt.Print("text/template: ")
t1.Execute(os.Stdout, payload)
fmt.Println()
// html/template — safe
t2, _ := htmlTpl.New("").Parse("<p>User says: </p>")
fmt.Print("html/template: ")
t2.Execute(os.Stdout, payload)
fmt.Println()
Expected output:
text/template: <p>User says: <script>alert('xss')</script></p>
html/template: <p>User says: <script>alert('xss')</script></p>
Checkpoint: Run go run .. The text/template line contains a raw <script> tag — dangerous in a browser. The html/template line contains <script> — completely safe. This is why you always use html/template for web output.
Lab 10: Full HTML Page with a Product Table
What you’ll practise: Rendering a complete HTML page with html/template, using ,, and dynamic CSS classes to style table rows based on data.
Task:
Render a full <!DOCTYPE html> page containing a table of products. Rows where InStock is false should receive a "out-stock" CSS class. Use `` to conditionally assign classes and display status text.
Steps:
- Define
type Product struct { Name string; Price float64; InStock bool }. - Write an
html/templatewith a<table>that ranges over[]Product. - Use
in-stockout-stockas theclassattribute on each<tr>. - Register a
"formatPrice"FuncMap function that formatsfloat64as"$XX.XX". - Execute to
os.Stdoutand inspect the source withgo run . | grep class.
const pageTpl = `<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Products</title></head>
<body>
<table>
<thead><tr><th>Name</th><th>Price</th><th>Status</th></tr></thead>
<tbody>
<tr class="in-stockout-stock">
<td></td>
<td></td>
<td>In StockOut of Stock</td>
</tr>
</tbody>
</table>
</body>
</html>`
funcs := template.FuncMap{
"formatPrice": func(f float64) string { return fmt.Sprintf("$%.2f", f) },
}
t := template.Must(template.New("page").Funcs(funcs).Parse(pageTpl))
products := []Product{
{"The Go Programming Language", 49.99, true},
{"Go in Action", 44.99, false},
{"Learning Go, 2nd Edition", 39.99, true},
}
t.Execute(os.Stdout, products)
Expected output (abbreviated):
<!DOCTYPE html>
<html lang="en">
...
<tr class="in-stock">
<td>The Go Programming Language</td>
<td>$49.99</td>
<td>In Stock</td>
</tr>
<tr class="out-stock">
<td>Go in Action</td>
<td>$44.99</td>
<td>Out of Stock</td>
</tr>
...
Checkpoint: go run . | grep class shows in-stock and out-stock classes on the correct rows. Try injecting <b> tags in a product name — html/template escapes them safely.
Lab 11: Embedded HTML Templates and an HTTP Server
What you’ll practise: Combining //go:embed, html/template, and net/http into a working web server that renders a dynamic HTML page from embedded template files.
Task:
Store the HTML template from Lab 10 as templates/index.html. Embed the templates/ directory with //go:embed. Parse it with template.ParseFS. Wire it into an HTTP handler that executes the template on each request.
Steps:
- Create
templates/index.htmlwith the product table template (adapt from Lab 10). - Add
//go:embed templatesandvar tmplFS embed.FSto the source file. - Parse with
template.Must(template.New("").Funcs(funcs).ParseFS(tmplFS, "templates/*.html")). - In the handler, execute with
tmpl.ExecuteTemplate(w, "index.html", products). - Serve
/on:8080. Test withcurl -s http://localhost:8080/ | grep "<td>".
//go:embed templates
var tmplFS embed.FS
func main() {
funcs := template.FuncMap{
"formatPrice": func(f float64) string { return fmt.Sprintf("$%.2f", f) },
}
tmpl := template.Must(
template.New("").Funcs(funcs).ParseFS(tmplFS, "templates/*.html"),
)
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
tmpl.ExecuteTemplate(w, "index.html", products)
})
log.Println("listening on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
Expected output:
$ curl -s http://localhost:8080/ | grep "<td>"
<td>The Go Programming Language</td>
<td>$49.99</td>
<td>In Stock</td>
...
Checkpoint: go run . starts the server. Open http://localhost:8080/ in a browser — the product table renders. Delete templates/index.html from disk and restart — the server still works because the template is embedded in the binary.
Section 4: Build Tags
Build tags (also called build constraints) tell the Go toolchain to include or exclude a source file from compilation. They are declared at the very top of a file, before the package clause, using the //go:build directive.
Syntax
//go:build linux
//go:build !windows
//go:build linux && amd64
//go:build linux || darwin
//go:build integration || e2e
//go:build prod
&&means “and” (both conditions must be true)||means “or” (either condition is true)!means “not”- Parentheses group expressions:
//go:build (linux || darwin) && amd64
Built-in tags
| Tag | Set when |
|---|---|
linux, darwin, windows, … |
GOOS matches |
amd64, arm64, 386, … |
GOARCH matches |
go1.21, go1.22, … |
toolchain version is at least that version |
Custom tags
Any identifier not matching a built-in tag is a custom tag:
go build -tags prod .
go build -tags "integration e2e" .
go test -tags integration ./...
File-naming shortcuts
A file named foo_linux.go is automatically constrained to GOOS=linux without needing a //go:build directive. Similarly foo_amd64.go constrains on GOARCH=amd64. The convention is name_GOOS.go, name_GOARCH.go, or name_GOOS_GOARCH.go.
Cross-compilation
GOOS=linux GOARCH=amd64 go build -o app-linux .
GOOS=darwin GOARCH=arm64 go build -o app-darwin .
GOOS=windows GOARCH=amd64 go build -o app.exe .
No external toolchain is needed — Go ships support for all major OS/arch combinations.
Lab 12: OS-Specific Code with Build Tags
What you’ll practise: Writing two implementations of the same function, each guarded by a build tag, and observing that the correct one compiles on each platform.
Task:
Define func osName() string in two files: platform_unix.go (compiles on everything except Windows) and platform_windows.go (compiles only on Windows). main.go calls osName() and prints the result.
Steps:
- Create
platform_unix.gowith//go:build !windowsat the top. Return"unix-like". - Create
platform_windows.gowith//go:build windowsat the top. Return"windows". - In
main.go(no build tag), callfmt.Println("Running on:", osName()). - Run
go run .— the right file compiles automatically based on your OS. - Cross-compile:
GOOS=windows go build -o app.exe .should succeed. Inspect which file was compiled withgo list -f '' ..
// platform_unix.go
//go:build !windows
package main
func osName() string { return "unix-like" }
// platform_windows.go
//go:build windows
package main
func osName() string { return "windows" }
// main.go
package main
import "fmt"
func main() {
fmt.Println("Running on:", osName())
}
Expected output (on Linux/macOS):
Running on: unix-like
Expected output (on Windows):
Running on: windows
Checkpoint: go run . prints the correct OS name. Run GOOS=windows go vet . — it passes. Run go list -f '' . to see exactly which .go files are in the current build.
Lab 13: Dev vs Prod Template Loading
What you’ll practise: Using a custom build tag to switch between two implementations of the same function — one reading templates from disk (dev) and one from an embedded FS (prod).
Task:
Create templates_dev.go (//go:build !prod) that returns os.DirFS("templates"), and templates_prod.go (//go:build prod) that uses //go:embed templates to return an embedded FS. Both expose func openTemplates() fs.FS. Wire this into the HTTP server from Lab 11.
Steps:
- Create
templates_dev.gowith build tag//go:build !prod. ImplementopenTemplates()usingos.DirFS. - Create
templates_prod.gowith build tag//go:build prod. Add//go:embed templatesand return the sub-FS. - In
main.go(no build tag), callopenTemplates()to get the FS, then parse templates from it. - Run
go run .— dev mode, templates reload on restart. - Run
go run -tags prod .— prod mode, templates are embedded.
// templates_dev.go
//go:build !prod
package main
import (
"fmt"
"io/fs"
"os"
)
func openTemplates() (fs.FS, error) {
fmt.Println("[dev] loading templates from disk")
return os.DirFS("templates"), nil
}
// templates_prod.go
//go:build prod
package main
import (
"embed"
"fmt"
"io/fs"
)
//go:embed templates
var embeddedTemplates embed.FS
func openTemplates() (fs.FS, error) {
fmt.Println("[prod] loading templates from embedded binary")
return fs.Sub(embeddedTemplates, "templates")
}
Expected output (dev mode):
[dev] loading templates from disk
listening on http://localhost:8080
Expected output (prod mode):
[prod] loading templates from embedded binary
listening on http://localhost:8080
Checkpoint: In dev mode, edit templates/index.html and restart — changes appear immediately. In prod mode, go build -tags prod -o server-prod . produces a binary that serves templates even when the templates/ directory is deleted.
Section 5: go:generate
//go:generate embeds a shell command in a Go source file. Running go generate ./... scans all .go files in the package tree and executes each directive. This is used to automate code generation — string enumeration, mocks, protocol buffer stubs, and build metadata.
Syntax
//go:generate <command> [arguments]
go generate sets several environment variables that generator scripts can read:
| Variable | Value |
|---|---|
$GOFILE |
The source file containing the directive |
$GOPACKAGE |
The package name |
$GODIR |
Absolute path to the directory |
$GOARCH |
Current GOARCH |
$GOOS |
Current GOOS |
Common generator programs
//go:generate stringer -type=Status // generate String() for an int enum
//go:generate mockgen -destination=... ... // generate interface mocks
//go:generate go run gen/version.go // run a local generator script
//go:generate protoc --go_out=. service.proto
Generator scripts with //go:build ignore
A generator is usually a standalone Go program in a subdirectory. Add //go:build ignore to prevent it from being compiled as part of the package it lives next to:
//go:build ignore
package main // its own program, not part of the host package
func main() { /* generate stuff */ }
Lab 14: Generate Version Info
What you’ll practise: Writing a generator script that writes a Go source file containing a build timestamp, then wiring it with //go:generate and importing the result.
Task:
Create gen/version.go (a standalone program) that writes internal/version/version.go containing const BuildTime = "<current UTC time>". Add //go:generate go run gen/version.go to main.go. Run go generate ./... then go run . to see the timestamp in the output.
Steps:
- Create
gen/version.gowith//go:build ignoreat the top. In itsmain(), formattime.Now().UTC()as RFC3339 and write it tointernal/version/version.go. - Create
internal/version/version.goas a placeholder withconst BuildTime = "1970-01-01T00:00:00Z". - Add
//go:generate go run gen/version.goabove thepackage mainline inmain.go. - Run
go generate ./...— the placeholder is overwritten with the current time. - Run
go run .— the server prints the build time at startup.
// gen/version.go
//go:build ignore
package main
import (
"fmt"
"os"
"time"
)
func main() {
const dst = "internal/version/version.go"
if err := os.MkdirAll("internal/version", 0o755); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
content := fmt.Sprintf(`// Code generated by gen/version.go; DO NOT EDIT.
package version
// BuildTime is the UTC timestamp from the last "go generate" run.
const BuildTime = %q
`, time.Now().UTC().Format(time.RFC3339))
if err := os.WriteFile(dst, []byte(content), 0o644); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
fmt.Println("generated", dst)
}
// internal/version/version.go (placeholder — overwritten by go generate)
package version
const BuildTime = "1970-01-01T00:00:00Z"
// main.go (excerpt)
//go:generate go run gen/version.go
package main
import "github.com/mmussett/zero2hero-golang/day-28/internal/version"
func main() {
fmt.Println("build time:", version.BuildTime)
// ...
}
Expected output:
$ go generate ./...
generated internal/version/version.go
$ go run .
build time: 2026-10-01T09:45:12Z
____ ____ ___
...
listening on http://localhost:8080
Checkpoint: Run go generate ./..., then inspect internal/version/version.go — the BuildTime constant should show the current time. Run go run . and confirm the time appears in the startup log.
Final Project: Go Book Store
Bring everything together: a fully self-contained web application that demonstrates every technique from today’s labs.
What it does
- Serves a product catalogue at
/ - Accepts a
?q=query parameter to filter products by name - Renders an HTML page using
html/templatewith a custom FuncMap - Serves static assets (CSS, JS) from
assets/ - Prints an ASCII-art banner from
assets/banner.txtat startup - Displays the build timestamp from
internal/version - Switches between disk-based (dev) and embedded (prod) modes via build tag
- Exposes a
//go:generatedirective that refreshes the build timestamp
File structure
day-27/
├── go.mod
├── main.go ← no build tag: main(), types, handlers, FuncMap
├── main_prod.go ← //go:build prod: embed + openAssets/openTemplates
├── templates_dev.go ← //go:build !prod: openAssets/openTemplates from disk
├── assets/
│ ├── banner.txt ← ASCII art (embedded and printed at startup)
│ ├── index.html ← static fallback
│ ├── style.css
│ └── script.js
├── templates/
│ └── index.html ← html/template source
├── gen/
│ └── version.go ← generator (//go:build ignore)
└── internal/
└── version/
└── version.go ← generated — const BuildTime
Build and run
# Dev mode — templates and assets read from disk (fast iteration)
go run .
# Prod mode — everything embedded, self-contained binary
go run -tags prod .
# Build a prod binary
go build -tags prod -o bookstore-prod .
# Regenerate the build timestamp
go generate ./...
# Cross-compile
GOOS=linux GOARCH=amd64 go build -tags prod -o bookstore-linux .
Key design decisions
Single main() in main.go (no build tag): The two build-tag files only define openAssets() and openTemplates() — the shared entry point and all business logic live in a tag-free file.
template.ParseFS after Funcs: The FuncMap must be registered before parsing because template functions are resolved at parse time, not execution time.
html/template not text/template: The product names and search query come from HTTP parameters — always use html/template when output goes to a browser.
fs.Sub to strip directory prefix: Both dev and prod implementations use fs.Sub internally so callers always receive a rooted FS regardless of how the files were loaded.
Official Documentation
embed package
embed— The//go:embeddirective andFStype. Start here for the rules on what can and cannot be embedded.- Go Blog: Using Go Embed — Official walkthrough with examples of all three embedding targets (
[]byte,string,embed.FS).
io/fs
io/fs— TheFSinterface,ReadFile,ReadDir,WalkDir,Sub, andGlob.embed.FSimplements this interface, making it interchangeable withos.DirFSfor local development.
text/template
text/template— Full reference for template syntax, actions, pipelines, FuncMap, and the parse/execute API.- Go Blog: The Go Template Language — Deep dive into template composition, context, and custom functions.
html/template
html/template— Same API astext/templatewith context-aware HTML, URL, JS, and CSS escaping.- OWASP XSS Prevention — Why escaping matters and how
html/templateimplements it.
go:generate
go generate— Official command reference, environment variables, and design rationale.- Go Blog: Generating code — Explains the philosophy and common patterns for code generation in Go.
Build constraints
- Build constraints — Complete reference for
//go:buildsyntax, boolean operators, GOOS/GOARCH tags, and the-tagsflag. - Go spec: Build constraints — The canonical list of built-in constraint tags.
Cross-compilation
- Go downloads — Official Go toolchain. Includes support for all GOOS/GOARCH targets with no extra setup.
go env GOARCH GOOS— Print the current build target. Override withGOOS=linux GOARCH=amd64 go build .