没有不值得去解决的问题,也没有不值得去学习的技术!

Getting Started With the Go Template Engine


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/template and text/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:

  1. Define the template file: Create a tmpl.html file with embedded directives (such as {{ . }}).
  2. Parse the template file: Call template.ParseFiles() to load the template.
  3. 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:

ExtensionDescription
.gotmplBest recommendation. Recognized by default by Go’s official language server gopls, semantically clear, and offers good consistency across IDEs.
.tmplHistorically common, but some editors require manual configuration for syntax highlighting.
.gohtmlSuitable 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 NameUse CaseSecurity Features
text/templatePlain text output (configuration files, scripts, code generation, logs, etc.)No automatic escaping; outputs as-is
html/templateHTML output in web applicationsContext-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:

HTML
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Go 模板示例</title>
</head>
<body>
    <!-- {{ . }} 是最基本的动作,输出传入的数据 -->
    <h1>{{ . }}</h1>
</body>
</html>

Go server-side code:

Go
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:

Bash
go run main.go

The terminal will output something similar to the following (no errors means the server started successfully):

Plaintext
# No output means success, or you can add log.Println for an explicit prompt

Now, open your browser and enter the following in the address bar:

Plaintext
http://localhost:8080

Press 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.

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.

Go
t := template.Must(template.ParseFiles("tmpl.html"))
// 现在可以放心地调用 t.Execute

This 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 *Template uses 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 example template.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 via ParseFiles).
  • t.ExecuteTemplate(w, name, data): Executes a specific template by its name.
Go
// 示例: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.

Go
// 创建并添加多个命名模板
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:

Go
{{ $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.

Go
{{/* 这里是一段注释,不会输出到 HTML */}}

4.4 Viewing Go Template Syntax from a PHP Perspective

ScenarioPHP (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.

Go
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:

HTML
<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:

Go
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 DimensionPHPGo (html/template)
Automatic escapingRequires manually calling htmlspecialchars()Automatic escaping by default (context-aware)
Misuse riskDevelopers might forget to call itSafe by default; security is the norm, not a choice
Bypassing security{!! $html !!} (Blade)template.HTML type annotation
Trust requirementsDevelopers must remain vigilant at all timesType 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:

  1. Two engines: text/template (plain text) and html/template (safe HTML)—same API, different scenarios.
  2. Three-step workflow: Define → Parse (ParseFiles) → Execute (Execute), repeating as necessary.
  3. Syntax basics: {{ . }} to access data, {{ $var }} to define variables, {{/* 注释 */}} to aid understanding.
  4. Security advantages: Context-aware automatic escaping fundamentally prevents XSS, an essential new mindset for PHP developers.

关于作者 · 持续学习与技术实践

我是一名拥有 15 年以上开发经验的技术从业者,自 2013 年起持续运营个人技术博客,记录工作与个人项目中遇到的真实问题,以及解决问题过程中的探索、实践和思考。

除了技术写作,我也在持续开发和维护自己的独立项目,探索新技术和新工具在实际场景中的应用。我始终相信,没有不值得去解决的问题,也没有不值得去学习的技术。

如果这篇文章对你有所帮助,欢迎浏览博客中的其他文章,也欢迎交流技术经验与想法。

关于我  ·  GitHub  ·  邮件联系