This series of articles records the author’s insights and practical experience learning the Go standard library. Starting with Go’s built-in template engines
html/templateandtext/template, this post guides you through the core concepts and practical usage, incorporating the perspective of a developer transitioning from PHP to Go.
Introduction: From PHP Mixing to Go Template Separation
If you, like me, transitioned from PHP to Go, you likely have mixed feelings about the term “template engine.” PHP itself is a template engine—you can embed <?php ?> tags directly into HTML at any time and mix in business logic. Laravel’s Blade and ThinkPHP’s ThinkTemplate are essentially further encapsulations and constraints on this native mixing capability.
Go, however, took a different path. It does not build the template system into the language syntax; instead, it provides two independent template engine packages in the standard library: text/template and html/template. This means template rendering is explicit, controllable, and type-safe.
This design philosophy is very Go: keep things simple and clear, leaving complex choices to the developer.
1. What is Go’s Template Engine
Go’s official template engine is essentially a data-driven text generator. It combines a “template” (text with placeholders and directives) with “data” (any struct, map, variable, etc., in Go) to generate the final output.
In web development, the workflow of a template engine is as follows:
- Define the template file: Create a
tmpl.htmlfile with embedded directives (such as{{ . }}). - Parse the template file: Call
template.ParseFiles()to load the template. - Execute with data: Call
t.Execute()to generate the final output.
📁 Recommended Template File Extensions
The official Go language does not enforce a specific file extension for templates. However, for project maintainability and cross-editor compatibility, the community has gradually adopted the following recommended conventions:
| Extension | Description |
|---|---|
.gotmpl | Best recommendation. Recognized by default by Go’s official language server gopls, semantically clear, and offers good consistency across IDEs. |
.tmpl | Historically common, but some editors require manual configuration for syntax highlighting. |
.gohtml | Suitable for HTML output scenarios and friendly to frontend tools. |
For new projects, it is recommended to uniformly use .gotmpl. The examples in this article still use the .html extension for compatibility with common tutorials, but you can adjust this according to your project specifications.
Go’s template engine sits between “no business logic” and “embedded business logic”—it supports basic control structures like conditional branching and loops, but discourages stuffing complex business logic into templates. This is a pragmatic trade-off.
2. Two Template Engines: text/template vs html/template
Go provides two template packages that share the same API and syntax but have entirely different purposes:
| Package Name | Use Case | Security Features |
|---|---|---|
text/template | Plain text output (configuration files, scripts, code generation, logs, etc.) | No automatic escaping; outputs as-is |
html/template | HTML output in web applications | Context-aware automatic escaping to prevent XSS attacks |
The core principle is: use html/template for HTML output, and use text/template for plain text output. Never mix them.
Getting to Know html/template: A Minimal Example
Create a tmpl.html file:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Go 模板示例</title>
</head>
<body>
<!-- {{ . }} 是最基本的动作,输出传入的数据 -->
<h1>{{ . }}</h1>
</body>
</html>Go server-side code:
package main
import (
"html/template"
"net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
// 1. 解析模板文件,返回 *Template 指针
t, _ := template.ParseFiles("tmpl.html")
// 2. 执行模板,将数据写入 ResponseWriter
t.Execute(w, "Hello, Go 模板引擎!")
}
func main() {
http.HandleFunc("/", handler)
http.ListenAndServe(":8080", nil)
}Save the above code as main.go, and create a tmpl.html file in the same directory (with the content shown above). Then execute:
go run main.goThe terminal will output something similar to the following (no errors means the server started successfully):
# No output means success, or you can add log.Println for an explicit promptNow, open your browser and enter the following in the address bar:
http://localhost:8080Press Enter, and the browser page will display:
Hello, Go template engine!
If you see this text on your page, it means your first Go template engine example has run successfully. See Figure 1.

Handling Errors with Must
The example above intentionally omits error handling (t, _ := ...), which is unsafe in a production environment. The standard library provides a convenience function template.Must: if an error occurs while parsing the template, it will directly trigger panic, preventing a nil pointer from appearing when Execute is called later.
t := template.Must(template.ParseFiles("tmpl.html"))
// 现在可以放心地调用 t.ExecuteThis design is well-suited for the global initialization phase in init() or main().
3. Deep Dive into the Template Execution Mechanism
3.1 Multiple Ways to Parse Templates
The template package provides several functions to parse template sources:
template.ParseFiles(filename ...string): Parses one or more template files. If multiple files are passed, the returned*Templateuses the first file name as the template name, and the remaining files are stored as associated templates.template.ParseGlob(pattern string): Batch parses templates using wildcards, for exampletemplate.ParseGlob("templates/*.html").template.New(name).Parse(text): First creates a named template, then parses it from a string.
3.2 Two Ways to Execute Templates
When there are multiple templates in a template collection, you need to distinguish between the execution methods:
t.Execute(w, data): Executes the default template in the collection (usually the first file passed viaParseFiles).t.ExecuteTemplate(w, name, data): Executes a specific template by its name.
// 示例:ParseFiles 传入了两个文件,但默认模板是第一个
t := template.Must(template.ParseFiles("layout.html", "content.html"))
// 执行 layout.html,而非自动执行为第一个
t.ExecuteTemplate(w, "layout.html", data)3.3 Template Naming and Scope
Another key point in Go’s template system is that *template.Template is essentially a named collection of templates, not a single template.
// 创建并添加多个命名模板
t := template.New("base")
t = template.Must(t.Parse(`{{define "header"}}头部内容{{end}}`))
t = template.Must(t.Parse(`{{define "footer"}}底部内容{{end}}`))
// 执行任意命名模板
t.ExecuteTemplate(os.Stdout, "header", nil)This design provides the foundation for template reuse and layout inheritance.
4. Template Syntax Basics (Opening Preview)
This first post will familiarize you with the two most core syntax elements; subsequent articles will explore them in depth.
4.1 Actions: {{ }}
All template directives are written within {{ and }}, and are called “actions.” Spaces are allowed inside and outside actions, as the Go template parser has a certain tolerance for whitespace.
{{ . }}: Outputs the current data context (the dot).{{ .FieldName }}: Accesses fields of a data structure; the field name must start with a capital letter (exported).{{ .Method }}: Can call methods whose receiver is the current value.
4.2 Variables and Pipelines: {{ $var := .Field }}
In Go templates, you can use variables starting with $ to store intermediate results and chain multiple operations together via pipelines:
{{ $author := .Author.Name }}
{{ $author | printf "作者:%s" }}The pipeline style closely resembles Unix pipes, allowing you to chain transformations from left to right.
4.3 Comments: {{/* 注释内容 */}}
Template comments do not appear in the final output, making them suitable for annotating the template’s logic.
{{/* 这里是一段注释,不会输出到 HTML */}}4.4 Viewing Go Template Syntax from a PHP Perspective
| Scenario | PHP (Blade) | Go Template |
|---|---|---|
| Output variable | {{ $name }} | {{ .Name }} |
| Loop | @foreach($users as $user) | {{ range .Users }} |
| Condition | @if($active) | {{ if .Active }} |
| Comment | {{-- 注释 --}} | {{/* 注释 */}} |
| Inheritance | @extends('layout') | Use {{ define }} + {{ template }} |
Accessing variables in Go templates requires capitalizing the first letter (due to reflection mechanism constraints), whereas PHP does not have this restriction. This is often a common pitfall for developers transitioning from PHP to Go.
5. Security Mechanisms of html/template
5.1 Automatic Escaping: Security as the Default Behavior
The most powerful feature of html/template is its context-aware automatic escaping. When parsing, the template engine analyzes the location of each {{ .Field }} (inside an HTML tag, within an attribute value, in a JavaScript block, or in a CSS style block) and automatically selects the most appropriate escaping strategy.
data := struct {
UserInput string
}{
UserInput: `<script>alert('XSS')</script>`,
}
t.Execute(w, data)If UserInput is placed inside a standard HTML tag (such as <div>{{ .UserInput }}</div>), the output will be escaped to:
<div><script>alert('XSS')</script></div>The script will not execute, fundamentally defending against XSS attacks.
5.2 When to Bypass Escaping: The template.HTML Type
If you genuinely need to output raw HTML (such as content generated by a backend rich text editor), you can use the template.HTML type. However, the prerequisite is that you fully trust the content and have already sanitized it as necessary:
import "html/template"
type PageData struct {
SafeHTML template.HTML // 原样输出,不会转义
RawText string // 会被自动转义
}
data := PageData{
SafeHTML: template.HTML("<strong>加粗文字</strong>"),
RawText: "<strong>会被转义</strong>",
}5.3 Security Enhancements in Go 1.26
As of this writing, html/template in Go 1.26 includes a security enhancement for a specific meta refresh scenario:
CVE-2026-27142: When inserting a URL into the content attribute of
<meta http-equiv="refresh" content="url=...">, certain edge cases could cause escaping to fail, posing an XSS risk. This issue has been fixed in 1.25.8 and 1.26.1.
If you are using Go 1.26.0 (excluding 1.26.1), it is recommended to upgrade to the latest patch version. If you cannot upgrade for special reasons, you can temporarily disable this escaping mechanism by setting the GODEBUG environment variable htmlmetacontenturlescape=0.
5.4 Comparison with PHP: Similarities and Differences in XSS Protection
| Comparison Dimension | PHP | Go (html/template) |
|---|---|---|
| Automatic escaping | Requires manually calling htmlspecialchars() | Automatic escaping by default (context-aware) |
| Misuse risk | Developers might forget to call it | Safe by default; security is the norm, not a choice |
| Bypassing security | {!! $html !!} (Blade) | template.HTML type annotation |
| Trust requirements | Developers must remain vigilant at all times | Type system enforces a security contract |
The most fundamental difference in Go’s approach is that security is not an option you “remember to do,” but the default behavior of the engine.
6. Summary
Today, we systematically covered the core concepts of Go’s built-in template engine from scratch:
- Two engines:
text/template(plain text) andhtml/template(safe HTML)—same API, different scenarios. - Three-step workflow: Define → Parse (
ParseFiles) → Execute (Execute), repeating as necessary. - Syntax basics:
{{ . }}to access data,{{ $var }}to define variables,{{/* 注释 */}}to aid understanding. - Security advantages: Context-aware automatic escaping fundamentally prevents XSS, an essential new mindset for PHP developers.
