深入详解 Go Template 模板语法及使用
Go 标准库的text/template与html/template是数据驱动的模板引擎,广泛用于文本生成、Web 页面渲染等场景。
本文基于 Go 官方定义的完整语法体系,结合代码示例,讲解说明 Go 模板语法指南,涵盖从基础分隔符到高级递归模板的所有特性
一、前提
在深入语法前,必须先明确 Go 模板的核心基础 —— 两大包的定位与语法设计本质,这是理解所有语法的前提:
| 包名 | 核心定位 | 语法支持 | 核心差异点 | 官方设计目标 |
|---|---|---|---|---|
text/template |
通用文本模板引擎 | 支持所有基础语法与高级特性 | 无自动转义,输出原始文本 | 适配配置文件、日志、邮件正文等非 HTML 场景 |
html/template |
安全 HTML 模板引擎 | 完全继承text/template所有语法 |
上下文感知自动转义,防 XSS 注入 | 适配 Web 页面、HTML 邮件等场景 |
语法本质:Go 模板语法是 “数据占位符 + 逻辑控制语句” 的组合,所有动态逻辑均包裹在{{ }}分隔符内,静态文本直接保留原样。
语法设计遵循 “极简逻辑 + 强安全约束”,不支持复杂计算,仅专注于数据渲染与基础流程控制。
二、基础语法
(一)分隔符与空白控制
Go 模板默认使用{{ }}作为动态语法分隔符,同时支持通过{{-和-}}控制空白字符(空格、换行、制表符):
{{- 语法 -}}:同时移除语法块前后的空白{{- 语法 }}:仅移除语法块前面的空白{{ 语法 -}}:仅移除语法块后面的空白- 未加
-:保留所有空白字符(包括语法块前后的换行、空格)
示例:
package main
import (
"os"
"text/template"
)
func main() {
tplStr := `
基础用法:{{ 10 + 20 }}
移除前面空白:{{- 10 + 20 }}
移除后面空白:{{ 10 + 20 -}}
移除前后空白:{{- 10 + 20 -}}
`
tpl := template.Must(template.New("blankTest").Parse(tplStr))
tpl.Execute(os.Stdout, nil)
}结果:
基础用法: 30 移除前面空白:30 移除后面空白: 30 移除前后空白:30
(二)注释语法
模板注释的官方语法为{{/* 注释内容 */}},注释内容不会被渲染,且支持跨行,但需注意空白控制:
- 普通注释:
{{/* 这是单行注释 */}} - 多行注释:
{{/* 这是跨多行的注释, 不会被渲染到输出结果中 */}} - 无空白注释:
{{- /* 注释前后无空白 */ -}}(避免注释占用空行)
注意:官方强调,注释本身会占用文本位置,若未加空白控制,可能导致输出多余空行,建议在模板中使用{{- /* 注释 */ -}}格式。
(三)作用域与 “.”
Go 模板的 “作用域” 是官方语法的核心,所有动态操作均依赖作用域上下文,而.(点)是作用域的核心标识:
.的官方定义:当前作用域的 “当前对象”,类似 Java 的this、Python 的self,本质是作用域内可访问的数据源。- 作用域类型:
- 顶级作用域:
Execute方法传入的第二个参数,整个模板全局可通过$访问($是顶级作用域的永久引用,不会随局部作用域变化)。 - 局部作用域:
range、with、block、define定义的模板会创建独立局部作用域,局部作用域内的.会被重定向为该结构的数据源。
- 顶级作用域:
- 作用域继承规则:局部作用域可访问外层作用域的变量(包括
$),但外层作用域无法访问局部作用域的变量。
示例
package main
import (
"os"
"text/template"
)
type User struct {
Name string
Hobbies []string
Profile Profile
}
type Profile struct {
Age int
City string
}
func main() {
tplStr := `
顶级作用域:{{ .Name }}(.指向User对象)
{{ range .Hobbies }}
range局部作用域:{{ . }}(.指向当前Hobby元素)
局部作用域访问顶级变量:{{ $.Name }}(通过$访问顶级User)
{{ end }}
{{ with .Profile }}
with局部作用域:{{ .Age }}-{{ .City }}(.指向Profile对象)
局部作用域访问外层变量:{{ $.Hobbies }}(通过$访问顶级Hobbies)
{{ end }}
`
user := User{
Name: "李明",
Hobbies: []string{"读书", "跑步", "编程"},
Profile: Profile{Age: 30, City: "北京"},
}
tpl := template.Must(template.New("scopeTest").Parse(tplStr))
tpl.Execute(os.Stdout, user)
} 三、变量语法
1. 变量定义与赋值(官方语法格式)
- 定义变量:
{{ $变量名 := pipeline }}(pipeline是产生数据的表达式,如{{ $name := .Name }}) - 重新赋值:
{{ $变量名 = pipeline }}(仅能对已定义变量赋值) - 命名规则:变量名以
$开头,后跟字母、数字、下划线,区分大小写(如$UserName、$user_age) - 特殊变量:
$(固定指向顶级作用域对象,不可重新赋值)
2. 变量作用域规则(官方明确约束)
- 变量仅在其定义的作用域及子作用域内有效,作用域结束(
end、}})后失效。 - 子作用域可定义与父作用域同名变量,会覆盖父作用域变量(局部优先)。
- 跨模板变量隔离:通过
define定义的独立模板,变量互不继承(包括$,但$的值由执行模板时传入的参数决定)。
示例:
tplStr := `
{{ $globalVar := "顶级变量" }}
顶级作用域:{{ $globalVar }}
{{ range .Hobbies }}
父作用域变量:{{ $globalVar }}(子作用域可访问)
{{ $localVar := . }}(子作用域定义局部变量)
子作用域变量:{{ $localVar }}
{{ $globalVar = "被子作用域修改" }}(修改父作用域变量)
{{ end }}
顶级作用域变量(被修改后):{{ $globalVar }}
{{/* 子作用域变量$localVar此处不可访问,会报错 */}}
`四、管道(Pipeline)语法
Go 模板的 “管道” 是数据传递核心,所有能产生数据的表达式都是管道,支持多管道串联,语法与 Unix 管道一致:
1. 管道的官方定义与分类
- 基础管道:
{{ . }}(当前对象)、{{ .Name }}(对象字段)、{{ len . }}(内置函数调用)、{{ "字符串" }}(直接量) - 复合管道:通过
|串联多个管道,前一个管道的输出作为后一个管道的最后一个参数(如{{ .Age | printf "%d岁" }}) - 括号管道:通过
()包裹复杂管道,优先执行(如{{ printf "%d+%d=%d" 1 2 (add 3 4) }},其中(add 3 4)是优先执行的管道)
2. 管道的官方约束与特性
- 管道支持多返回值:若管道返回多个值,第二个返回值必须是
error类型,模板引擎会自动处理错误(报错并终止渲染)。 - 空管道处理:若管道返回空值(0、nil、空字符串、空切片等),后续依赖该管道的操作会按 “空值逻辑” 处理(如
if判断为 false)。
示例
tplStr := `
基础管道:{{ .Name }}
函数管道:{{ .Age | printf "年龄:%02d" }}
多管道串联:{{ .Name | strings.ToUpper | printf "大写姓名:%s" }}
括号管道:{{ printf "%s(长度:%d)" .Name (len .Name) }}
错误管道:{{ .Age | invalidFunc }}(第二个返回值为error,会报错)
`
// 需注册自定义函数strings.ToUpper(模板默认不支持,需手动注册)
tpl := template.New("pipelineTest").Funcs(template.FuncMap{
"strings.ToUpper": strings.ToUpper,
})五、流程控制语法
Go 模板支持 4 类官方定义的流程控制语句,覆盖条件判断、循环、作用域切换、模板默认实现,语法简洁且严格:
(一)if-else if-else
条件判断
{{/* 格式1:单分支 */}}
{{ if pipeline }} 满足条件时执行 {{ end }}
{{/* 格式2:双分支 */}}
{{ if pipeline }} 满足条件时执行 {{ else }} 不满足条件时执行 {{ end }}
{{/* 格式3:多分支(官方推荐写法) */}}
{{ if pipeline1 }} 满足条件1 {{ else if pipeline2 }} 满足条件2 {{ else }} 都不满足 {{ end }}
{{/* 格式4:嵌套if(等价于else if,官方不推荐,建议用格式3) */}}
{{ if pipeline1 }} 满足条件1 {{ else }} {{ if pipeline2 }} 满足条件2 {{ end }} {{ end }}注意末尾需要加上 {{end}}
条件判断规则:管道返回 “非空值”(非 0、非 nil、非空字符串 / 切片 / Map)时,if判断为 true,否则为 false。
(二)range
循环遍历
支持遍历切片、数组、Map、Channel,完整语法:
{{/* 格式1:基础遍历(仅取元素值) */}}
{{ range pipeline }} 循环体(.指向当前元素) {{ end }}
{{/* 格式2:遍历+else(空集合时执行) */}}
{{ range pipeline }} 循环体 {{ else }} 集合为空时执行 {{ end }}
{{/* 格式3:遍历取索引/键(切片/数组取索引,Map取key) */}}
{{ range $index, $value := pipeline }} 索引:{{ $index }},值:{{ $value }} {{ end }}
{{/* 格式4:遍历Map取key和value */}}
{{ range $key, $value := .MapData }} key:{{ $key }},value:{{ $value }} {{ end }}注意末尾需要加上 {{end}}
扩展特性:循环体内支持{{ break }}(终止循环)和{{ continue }}(跳过当前迭代),这是官方在 Go 1.16 + 明确支持的语法。
示例
tplStr := `
{{ range $idx, $hobby := .Hobbies }}
{{ if eq $idx 1 }} {{ continue }} {{ end }}(跳过索引1的元素)
索引:{{ $idx }},爱好:{{ $hobby }}
{{ if eq $idx 2 }} {{ break }} {{ end }}(索引2后终止循环)
{{ else }}
无爱好数据
{{ end }}
`(三)with
作用域切换
用于临时切换当前作用域的.对象,完整语法:
{{/* 格式1:单分支(非空时切换) */}}
{{ with pipeline }} 作用域切换后(.指向pipeline结果) {{ end }}
{{/* 格式2:双分支(非空切换,空时执行else) */}}
{{ with pipeline }} 切换作用域:{{ . }} {{ else }} pipeline为空时执行 {{ end }}核心逻辑:若pipeline非空,将当前作用域的.赋值为pipeline的结果,执行循环体;若为空,跳过循环体(或执行 else)。
示例
tplStr := `
{{ with .Profile }}
切换后:{{ .Age }}岁(.指向Profile对象)
访问顶级对象:{{ $.Name }}(通过$访问)
{{ else }}
无Profile数据
{{ end }}
`(四)define
模板默认实现(官方语法糖)
官方定义define是 “定义模板 + 执行模板” 的语法糖,核心用于实现模板继承(父模板定义默认块,子模板覆盖),完整语法:
{{ define "模板名" }} 默认模板内容 {{ end }}
{{ block "模板名" pipeline }} 默认模板内容 {{ end }}以上三种效果是一样的,但是目前比较常用的是:define
在子模板使用定义的父模板
{{ template "模板名" pipeline }}一般情况下 pipeline 为 .
示例
// 父模板(base.tpl)
{{ define "base" }}
<!DOCTYPE html>
<html>
<head>
<title>{{ block "title" . }}默认标题{{ end }}</title>
</head>
<body>
{{ block "content" . }}默认内容{{ end }}
</body>
</html>
{{ end }}
// 子模板(index.tpl)这里表示引用了 父模板(base.tpl),并且 . 数据传入
{{ template "base" . }}
<h1>首页内容:{{ .Name }}</h1>
{{ end }}六、内置函数语法
Go 模板内置了 20 + 官方函数,覆盖布尔运算、比较运算、字符串处理、索引操作等场景,所有函数均遵循 “前缀语法”(函数名在前,参数在后),以下是完整函数列表:
(一)布尔运算函数
| 函数名 | 官方定义 | 示例 | 输出 |
|---|---|---|---|
| and | 多参数 “与”,返回第一个空值或最后一个值 | {{ and "a" "" "b" }} |
"" |
| or | 多参数 “或”,返回第一个非空值或最后一个值 | {{ or "" "a" "b" }} |
"a" |
| not | 布尔取反,参数为空值返回 true,否则 false | {{ not .Name }}(若.Name 为空) |
true |
示例
tplStr := `
{{ if and (.IsMan) (not .IsOldMan) }}
.IsMan=true && .IsOldMan=false 执行
{{ else }}
.IsMan=false || .IsOldMan=true 执行
{{ end }}
`(二)比较运算函数
| 函数名 | 官方定义 | 示例 | 输出 |
|---|---|---|---|
| eq | 等于(支持多参数:eq a b c → a==b 或 a==c) | {{ eq .Age 30 25 }}(.Age=30) |
true |
| ne | 不等于 | {{ ne .Age 30 }} |
false |
| lt | 小于(<) | {{ lt .Age 30 }} |
false |
| le | 小于等于(<=) | {{ le .Age 30 }} |
true |
| gt | 大于(>) | {{ gt .Age 30 }} |
false |
| ge | 大于等于(>=) | {{ ge .Age 30 }} |
true |
示例
tplStr := `
{{ if and (eq .Name 'fiveyoboy') (gt .Age 20) }}
Name='fiveyoboy' 并且 Age>20 执行
{{ else }}
反之执行
{{ end }}
`(三)字符串 / 文本函数
| 函数名 | 官方定义 | 示例 | 输出 |
|---|---|---|---|
| 等价于 fmt.Sprint,拼接参数为字符串 | {{ print "name:" .Name }} |
name:李明 |
|
| printf | 等价于 fmt.Sprintf,格式化输出 | {{ printf "age:%02d" .Age }} |
age:30 |
| println | 等价于 fmt.Sprintln,拼接并加换行 | {{ println .Name }} |
李明\n |
| len | 返回字符串 / 切片 / Map 的长度 | {{ len .Hobbies }}(3 个元素) |
3 |
(四)索引 / 访问函数
| 函数名 | 官方定义 | 示例 | 输出 |
|---|---|---|---|
| index | 访问可索引对象(切片 / 数组 / Map / 字符串) | {{ index .Hobbies 0 }}(第一个爱好) |
读书 |
支持多维索引:index .MapData "key" 1 |
{{ index .Profile "Age" }}(Map 字段) |
30 |
|
| slice | 切片操作(等价于 Go 的 slice [i:j:cap]) | {{ slice .Hobbies 1 3 }} |
[跑步 编程] |
(五) 函数调用函数
| 函数名 | 官方定义 | 示例 | 输出 |
|---|---|---|---|
| call | 调用非模板内置函数(如结构体方法、自定义函数) | {{ call .Calc 10 20 }}(.Calc 是结构体方法) |
30(假设方法返回和) |
(六)安全转义函数(仅 text/template 支持)
| 函数名 | 官方定义 | 示例 | 输出 |
|---|---|---|---|
| html | 将文本转义为 HTML 安全格式 | {{ html "<script>" }} |
<script> |
| js | 将文本转义为 JS 安全格式 | {{ js "'alert()'" }} |
\x27alert()\x27 |
| urlquery | 将文本转义为 URL 查询参数格式 | {{ urlquery "a b" }} |
a+b |
| css | 将文本转义为 CSS 安全格式 | {{ css "background: #fff;" }} |
background: #fff; |
常见问题
Q1、range 循环中修改外层变量生效?
range 子作用域可修改父作用域变量(因为变量是引用传递)
若需保留父作用域变量原值,在子作用域内先复制变量({{ $copy := $globalVar }})
Q2、html/template 转义导致原生 HTML 失效?
自动转义是安全特性,非 bug
确认内容安全后,使用template.HTML类型转换,而非改用text/template。
Q3、引用父模板后获取变量失效?
引用父模板应该同时传入当前变量 .
错误示例:({{ template "base" }}
正确示例:({{ template "base" . }}
大家在使用过程中还有哪些语法是本文没有提到的?欢迎大家在评论区进行交流沟通!!!
版权声明
未经授权,禁止转载本文章。
如需转载请保留原文链接并注明出处。即视为默认获得授权。
未保留原文链接未注明出处或删除链接将视为侵权,必追究法律责任!
本文原文链接: https://fiveyoboy.com/articles/go-template-guide/
备用原文链接: https://blog.fiveyoboy.com/articles/go-template-guide/