bigywater.tech
Go · 111

text/template:管道、变量与 FuncMap 的求值

text/template 的管道、变量和函数调用有一套自己的求值规则,和 Go 代码的直觉不完全一样:管道把值放在最后一个参数,变量用 := 和 = 的作用域不同,缺 key 默认不报错。这篇用一个个小模板验证这些规则,并量解析与执行的成本。

1-3
{{3 | printf "%d-%d" 1}} 的输出:管道的值是 printf 的最后一个参数
map 缺 key 时的默认输出;Option("missingkey=error") 才会报错
612 vs 3474 ns
已解析模板的 Execute 与每次都 Parse + Execute 的耗时

管道的参数位置

实测 · 本机执行输出
  {{.Name | printf "%q"}}            "小明"
  {{3 | printf "%d-%d" 1}}           1-3
  {{.Name | printf "%q" | len}}      8
  {{len .Tags | printf "标签数=%d"}}    标签数=2
  {{printf "%s=%d" .Name .Age}}      小明=20
  {{.Greeting}}                      你好,小明
  {{.PtrMethod}}(值 u)                输出 "",执行出错:template: t:1:2: executing "t" at <.PtrMethod>: can't evaluate field PtrMethod in type main.User
  {{.PtrMethod}}(&u)                 指针方法:小明

管道把前一个命令的结果放在后一个命令最后一个参数的位置:{{3 | printf "%d-%d" 1}} 得到 1-3。所以要在函数设计时把“被管道传进来的值”放在参数列表最后。方法也能直接调用,但指针接收者的方法在传值数据上会报 “can't evaluate field”。

变量作用域

实测 · 本机执行输出
  在 if 里用 := 声明                      1
  在 if 里用 = 赋值                       2
  range 里累加                          10
  range 的下标和值                        0=x;1=y;
  $ 指向根数据                            小明-a;小明-b;
  with 改变 .                          a|小明

:= 声明的变量到所在块的 {{end}} 为止,内层的 {{$x := 2}} 不会影响外面的 $x;= 才是改外面的变量(range 里累加就靠它);$ 始终指向传入的根数据。

缺失的东西

实测 · 本机执行输出
  map 缺 key(默认)                      <no value>
  map 缺 key(missingkey=error)        输出 "",执行出错:template: t:1:2: executing "t" at <.b>: map has no entry for key "b"
  map 缺 key(missingkey=zero)         0
  struct 缺字段                         输出 "",执行出错:template: t:1:2: executing "t" at <.Nope>: can't evaluate field Nope in type main.User
  nil 指针上取字段                         输出 "",执行出错:template: t:1:2: executing "t" at <.Name>: nil pointer evaluating *main.User.Name
  未定义的函数                             解析失败:template: t:1: function "nofunc" not defined

map 缺 key 默认输出 <no value>,要报错得设 missingkey=error;结构体缺字段在执行时报错;nil 指针取字段报错;未定义的函数在解析时就失败。所以模板里的拼写错误,有的要等到运行时才暴露。

真值与短路

实测 · 本机执行输出
  {{if .}} 对空字符串                     [假]
  {{if .}} 对 0                       [假]
  {{if .}} 对空切片                      [假]
  {{if .}} 对 nil                     [假]
  {{if .}} 对非空 map                   [真]
  {{if .}} 对结构体(零值)                  [真]
  or 短路:{{or true (side)}}           true
    side() 被调用了 0 次

空字符串、0、nil、空切片/map 为假,非空 map 与零值结构体为真;or/and 短路,{{or true (side)}} 里 side() 一次都没被调用。

FuncMap

实测 · 本机执行输出
  div 10 2                           5
  div 10 0(函数返回 error)               输出 "前缀",执行出错:template: t:1:8: executing "t" at <div 10 0>: error calling div: 除数为 0
  先 Parse 再 Funcs                    panic:template: t:1: function "up" not defined
  FuncMap 里函数返回 3 个值                 panic:function bad has 3 return values; should be 1 or 2

函数必须在 Parse 之前注册,否则解析时就 panic;函数可以返回一个值或两个值(第二个是 error),返回 error 会中断执行,中断前已经写出的输出仍然保留在 writer 里。

空白

实测 · 本机执行输出
  不修剪空白                              [\n小明\n]
  {{- 修剪左边 -}}                       [小明]

{{- 与 -}} 去掉动作两侧的空白(包括换行),生成纯文本格式时很常用。

成本

实测 · 本机执行输出
  已解析的模板 Execute:612 ns/次、6 次分配;fmt.Fprintf:77 ns/次、1 次分配;每次都 Parse + Execute:3474 ns/次、69 次分配

已解析的模板 Execute 约 612 ns,是 fmt.Fprintf 的约 8 倍;每次都 Parse 要 3474 ns、69 次分配。模板应该启动时解析一次,复用。

文档

实测 · 本机执行输出
  A pipeline may be "chained" by separating a sequence of commands with pipeline characters '|'. In a chained pipeline, the result of each command is passed as the last argument of the following command. The output of the final command in the pipeline is the val

文档对管道的描述与第一节的实验一致:链式管道里每个命令的结果是下一个命令的最后一个参数。

分步演示

text/template
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Go 1.27.1(brew);用 go run / go build / go test 直接编译运行,需要第三方库时经代理从 proxy.golang.org 下载;计时用 time.Now / testing.B,数字每次运行会有小幅抖动。每个例子是一个很小的模板加一份数据,输出由 Execute 得到(换行在输出中以 \n 显示);成本用 testing.Benchmark,模板是一行带 range 的文本;读过的文档:go doc text/template 中关于管道的一段
    • 没有验证:template 与 define/block 的嵌套模板、自定义分隔符、并发执行同一个模板的安全性、html/template 的转义(下一篇)、模板函数的类型转换规则、大模板的解析成本
    京ICP备2026067502号-1京公网安备11010502063487号