Go 语言标准库 text/template 包深入浅出

本文深入讲解了Go语言的模板引擎,介绍了模板的基本概念、数据驱动原理及各种模板操作,包括条件语句、循环语句、函数调用等,同时探讨了模板之间的引用机制和变量作用域。

作者:浮x尘
链接:https://juejin.im/post/5c403b98f265da612d1984c9

模板

什么是模板?

官方定义:

Package template implements data-driven templates for generating textual output.

template 包是数据驱动的文本输出模板,其实就是在写好的模板中填充数据。

下面是一个简单的模板示例:

// 模板定义
tepl := "My name is {{ . }}"

// 解析模板
tmpl, err := template.New("test").Parse(tepl)

// 数据驱动模板
data := "jack"
err = tmpl.Execute(os.Stdout, data)

{{ 和 }} 中间的句号 . 代表传入模板的数据,根据传入的数据不同渲染不同的内容。

. 可以代表 go 语言中的任何类型,如结构体、哈希等。

至于 {{ 和 }} 包裹的内容统称为 action,分为两种类型:

  • 数据求值(data evaluations)
  • 控制结构(control structures)

action 求值的结果会直接复制到模板中,控制结构和我们写 Go 程序差不多,也是条件语句、循环语句、变量、函数调用等等…

将模板成功解析(Parse)后,可以安全地在并发环境中使用,如果输出到同一个 io.Writer 数据可能会重叠(因为不能保证并发执行的先后顺序)。

Actions

模板中的 action 并不多,我们一个一个看。

注释

{{/* comment */}}

裁剪空格

// 裁剪 content 前后的空格
{{- content -}}

// 裁剪 content 前面的空格
{{- content }}

// 裁剪 content 后面的空格
{{ content -}}

文本输出

{{ pipeline }}

pipeline 代表的数据会产生与调用 fmt.Print 函数类似的输出,例如整数类型的 3 会转换成字符串 “3” 输出。

条件语句

{{ if pipeline }} T1 {{ end }}

{{ if pipeline }} T1 {{ else }} T0 {{ end }}

{{ if pipeline }} T1 {{ else if pipeline }} T0 {{ end }}

// 上面的语法其实是下面的简写
{{ if pipeline }} T1 {{ else }}{{ if pipeline }} T0 { {end }}{{ end }}

{{ if pipeline }} T1 {{ else if pipeline }} T2 {{ else }} T0 {{ end }}

如果 pipeline 的值为空,不会输出 T1,除此之外 T1 都会被输出。

空值有 false、0、任意 nil 指针、接口值、数组、切片、字典和空字符串 ""(长度为 0 的字符串)。

循环语句

{{ range pipeline }} T1 {{ end }}

// 这个 else 比较有意思,如果 pipeline 的长度为 0 则输出 else 中的内容
{{ range pipeline }} T1 {{ else }} T0 {{ end }}

// 获取容器的下标
{{ range $index, $value := pipeline }} T1 {{ end }}

pipeline 的值必须是数组、切片、字典和通道中的一种,即可迭代类型的值,根据值的长度输出多个 T1。

define

{{ define "name" }} T {{ end }}

定义命名为 name 的模板。

template

{{ template "name" }}

{{ template "name" pipeline }}

引用命名为 name 的模板。

block

{{ block "name" pipeline }} T1 {{ end }}

block 的语义是如果有命名为 name 的模板,就引用过来执行,如果没有命名为 name 的模板,就是执行自己定义的内容。

也就是多做了一步模板是否存在的判断,根据这个结果渲染不同的内容。

with

{{ with pipeline }} T1 {{ end }}

// 如果 pipeline 是空值则输出 T0
{{ with pipeline }} T1 {{ else }} T0 {{ end }}

{{ with arg }}
    . // 此时 . 就是 arg
{{ end }}

with 创建一个新的上下文环境,在此环境中的 . 与外面的 . 无关。

参数

参数的值有多种表现形式,可以求值任何类型,包括函数、指针(指针会自动间接取值到原始的值):

  • 布尔、字符串、字符、浮点数、复数的行为和 Go 类似
  • 关键字 nil 代表 go 语言中的 nil
  • 字符句号 . 代表值的结果
  • 以 $ 字符开头的变量则为变量对应的值
  • 结构体的字段表示为 .Field,结果是 Field 的值,支持链式调用 .Field1.Field2
  • 字典的 key 表示为 .Key 结果是 Key 对应的值
  • 如果是结构体的方法集中的方法 .Method 结果是方法调用后返回的值(The result is the value of invoking the method with dot as the receiver)**
    • 方法要么只有一个任意类型的返回值要么第二个返回值为 error,不能再多了,如果 error 不为 nil,会直接报错,停止模板渲染
    • 方法调用的结果可以继续链式调用 .Field1.Key1.Method1.Field2.Key2.Method2
    • 声明变量方法集也可以调用 $x.Method1.Field
    • 用括号将调用分组 print (.Func1 arg1) (.Func2 arg2)(.StructValuedMethod "arg").Field

这里最难懂的可能就是函数被调用的方式,如果访问结构体方法集中的函数和字段中的函数,此时的行为有什么不同?

写个 demo 测一下:

type T struct {
	Add func(int) int
}

func (t *T) Sub(i int) int {
	log.Println("get argument i:", i)
	return i - 1
}

func arguments() {
	ts := &T{
		Add: func(i int) int {
			return i + 1
		},
	}
	tpl := `
		// 只能使用 call 调用
		call field func Add: {{ call .ts.Add .y }}
		// 直接传入 .y 调用
		call method func Sub: {{ .ts.Sub .y }}
	`
	t, _ := template.New("test").Parse(tpl)
	t.Execute(os.Stdout, map[string]interface{}{
		"y": 3,
		"ts": ts,
	})
}

output:

call field func Add: 4
call method func Sub: 2

可以得出结论:如果函数是结构体中的函数字段,该函数不会自动调用,只能使用内置函数 call 调用。

如果函数是结构体方法集中的方法,会自动调用该方法,并且会将返回值赋值给 .,如果函数返回新的结构体、map,可以继续链式调用。

变量

action 中的 pipeline 可以初始化变量存储结果,语法也很简单:

$variable = pipeline

此时,这个 action 声明了一个变量而没有产生任何输出。

range 循环可以声明两个变量:

range $index, $element := pipeline

在 if、with 和 range 中,变量的作用域拓展到 {{ end }} 所在的位置。

如果不是控制结构,声明的变量的作用域会扩展到整个模板。

例如在模板开始时声明变量:

{{ $pages := .pagination.Pages }}
{{ $current := .pagination.Current }}

在渲染开始的时候,$ 变量会被替换成 . 开头的值,例如 $pages 会被替换成 .pagenation.Pages。所以在模板间的相互引用不会传递变量,变量只在某个特定的作用域中产生作用。

函数

模板渲染时会在两个地方查找函数:

  • 自定义的函数 map
  • 全局函数 map,这些函数是模板内置的

自定义函数使用 func (t *Template) Funcs(funcMap FuncMap) *Template 注册。

全局函数列表:

and

返回参数之间 and 布尔操作的结果,其实就是 JavaScript 中的逻辑操作符 &&,返回第一个能转换成 false 的值,在 Go 中就是零值,如果都为 true 返回最后一个值。

tpl := "{{ and .x .y .z }}"
t, _ := template.New("test").Parse(tpl)
t.Execute(os.Stdout, map[string]interface{}{
    "x": 1,
    "y": 0,
    "z": 3,
})

output:

0

or

逻辑操作符 ||,返回第一个能转换成 true 的值,在 Go 中就是非零值,如果都为 false 返回最后一个值。

tpl := "{{ or .x .y .z }}"
t, _ := template.New("test").Parse(tpl)
t.Execute(os.Stdout, map[string]interface{}{
    "x": 1,
    "y": 0,
    "z": 3,
})

output:

1

call

返回调用第一个函数参数的结果,函数必须有一个或两个回值(第二个返回值必须是 error,如果值不为 nil 会停止模板渲染)

tpl := "call: {{ call .x .y .z }} \n"
t, _ := template.New("test").Parse(tpl)
t.Execute(os.Stdout, map[string]interface{}{
    "x": func(x, y int) int { return x+y},
    "y": 2,
    "z": 3,
})

output:

5

html

返回转义后的 HTML 字符串,这个函数不能在 html/template 中使用。

js

返回转义后的 JavaScript 字符串。

index

在第一个参数是 array、slice、map 时使用,返回对应下标的值。

index x 1 2 3 等于 x[1][2][3]

len

返回复合类型的长度。

not

返回布尔类型参数的相反值。

print

等于 fmt.Sprint

printf

等于 fmt.Sprintf

println

等于 fmt.Sprintln

urlquery

对字符串进行 url Query 转义,不能在 html/template 包中使用。

// URLQueryEscaper returns the escaped value of the textual representation of
// its arguments in a form suitable for embedding in a URL query.
func URLQueryEscaper(args ...interface{}) string {
	return url.QueryEscape(evalArgs(args))
}

从源码可以看到这个函数直接调用 url.QueryEscape 对字符串进行转义,并没有什么神秘的。

比较函数

  • eq: ==
  • ge: >=
  • gt: >
  • le: <=
  • lt: <
  • ne: !=

分析两个源码:

// eq evaluates the comparison a == b || a == c || ...
func eq(arg1 reflect.Value, arg2 ...reflect.Value) (bool, error) {
	v1 := indirectInterface(arg1)
	k1, err := basicKind(v1)
	if err != nil {
		return false, err
	}
	if len(arg2) == 0 {
		return false, errNoComparison
	}
	for _, arg := range arg2 {
		v2 := indirectInterface(arg)
		k2, err := basicKind(v2)
		if err != nil {
			return false, err
		}
		truth := false
		if k1 != k2 {
			// Special case: Can compare integer values regardless of type's sign.
			switch {
			case k1 == intKind && k2 == uintKind:
				truth = v1.Int() >= 0 && uint64(v1.Int()) == v2.Uint()
			case k1 == uintKind && k2 == intKind:
				truth = v2.Int() >= 0 && v1.Uint() == uint64(v2.Int())
			default:
				return false, errBadComparison
			}
		} else {
			switch k1 {
			case boolKind:
				truth = v1.Bool() == v2.Bool()
			case complexKind:
				truth = v1.Complex() == v2.Complex()
			case floatKind:
				truth = v1.Float() == v2.Float()
			case intKind:
				truth = v1.Int() == v2.Int()
			case stringKind:
				truth = v1.String() == v2.String()
			case uintKind:
				truth = v1.Uint() == v2.Uint()
			default:
				panic("invalid kind")
			}
		}
		if truth {
			return true, nil
		}
	}
	return false, nil
}

// ne evaluates the comparison a != b.
func ne(arg1, arg2 reflect.Value) (bool, error) {
	// != is the inverse of ==.
	equal, err := eq(arg1, arg2)
	return !equal, err
}

eq 先判断接口类型是否相等,然后判断值是否相等,没什么特殊的地方。

ne 更是简单的调用 eq,然后取反。

ge、gt、le、lt 与 eq 类似,先判断类型,然后判断大小。

嵌套模板

下面是一个更复杂的例子:

// 加载模板
template.ParseFiles("templates/")

// 加载多个模板到一个命名空间(同一个命名空间的模块可以互相引用)
template.ParseFiles("header.tmpl", "content.tmpl", "footer.tmpl")

// must 加载失败时 panic
tmpl := template.Must(template.ParseFiles("layout.html"))

// 执行加载后的模板文件,默认执行第一个
tmpl.Execute(w, "test")

// 如果 tmpl 中有很多个模板,可以指定要执行的模板名
tmpl.ExecuteTemplate(w, "layout", "Hello world")

ExecuteTemplate 指定的名字就是模板文件中 define "name" 的 name。

总结

Parse 系列函数初始化的 Template 类型实例。

Execute 系列函数则将数据传递给模板渲染最终的字符串。

模板本质上就是 Parse 函数加载多个文件到一个 Tempalte 类型实例中,解析文件中的 define 关键字注册命名模板,命名模板之间可以使用 template 互相引用,Execute 传入对应的数据渲染。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值