GO Template 语法和基本使用

2024-05-27 笔记 GO

当前这个网站没有用本人最熟悉的前端框架来开发。一是觉得没必要,博客本来就没多少特效,与服务器交互几乎为零,并没有多少技术;二是感觉那些框架打包出来文件有点臃肿,租了服务器,部署了博客才知道带宽有多贵,那些动辄几MB的js文件有点浪费带宽了(虽然可以优化打包文件,但还是比这种臃肿、更大);三是我算是前端小白,维护过公司老项目,那个用的是Java生成的HTML文件,感觉很神奇还能这么用。所以开发博客时选择了这种老式开发方式([🐕狗头保命]),这篇文章用来做个笔记,记录一下用法,省的下次开发新功能时还要去之前模板、搜索引擎找,汇总一下用的的语法和使用方式。

所有代码都在GitHub上https://github.com/hongyun-robot/go-template博客目前部分html代码块渲染有bug。已修复

本文用gin来当作服务器、动态生成HTML。gin的解析模板还是html/template包,所以语法一样。

text
copy code
目录结构 --template --grammary.gohtml --layout.gohtml --header.gohtml --footer.gohtml --main.go
go
copy code
package main import ( "github.com/gin-gonic/gin" "net/http" ) func main() { r := gin.New() r.LoadHTMLGlob("template/*") r.GET("/grammar", func(c *gin.Context) { c.HTML(http.StatusOK, "layout", gin.H{}) }) // Listen and Server in 0.0.0.0:8080 r.Run(":8888") }

在文件中使用{{ define "name" }}{{ end }}来定义一个模板。

使用 {{ define "layout" }} 语法定义了模板文件。使用 {{ .hello }} 语法来渲染数据。

html
copy code
{{/* template/layout */}} {{ define "layout" }} <!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>go-template 语法</title> </head> <body> <p>{{ .hello }}</p> </body> </html> {{ end }}
go
copy code
r.GET("/grammar", func(c *gin.Context) { c.HTML(http.StatusOK, "layout", gin.H{ "hello": "Hello World", }) })

在上一个例子中使用了 {{ .hello }} 语法,这个里面的 . 表示当前全局作用域对象值。对于 gin 表示 gin.H{} 这个map 变量;对于 func (t *Template) Execute(wr io.Writer, data any) error 方法等价于 data 变量。

整个文件、range 模块、with 模块和 block 模块都是不同的作用域。习惯把整个文件作用域称为全局作用域,其他则称为局部作用域。

全局作用域值 . 称为 dot

!!! abstract 注意 在循环局部作用域中 dot 指向循环 pipelines 值而不是全局对象值,$ 指向全局对象值。建议用 $ 替换 dot 来进行使用。 !!!

使用 {{ template "name" pipeline? }} 语法调用其他模板,实现模板嵌套、重复使用、模板递归等操作。

pipeline 可以视为管道,将数据传递到调用模板里面,改变调用模板里面全局作用域值,将 . 设为传递过来的值。

可以将一个大模板拆分成多个小模版,维护更方便;将重复逻辑部分,要在不同位置使用的拆分出来,使用时直接调用模板。我觉得叫组件更适合。

定义了三个模板文件,grammary、header、footer。在 grammary 模板中引入了header和footer模板,并且将自己的全局作用域值dot传递给这两个模板。

html
copy code
{{/* template/grammar */}} {{ define "grammar" }} {{ template "header" . }} <p>由我来组成身体 {{ .hello }} 。</p> {{ template "footer" . }} {{ end }}
html
copy code
{{/* template/header */}} {{ define "header" }} <!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> </head> <body> <p>{{ .hello }}我来组成头部</p> {{ end }}
html
copy code
{{/* template/footer */}} {{ define "footer" }} <p>{{ .hello }}我来组成底部</p> </body> </html> {{ end }}
go
copy code
r.GET("/grammar", func(c *gin.Context) { c.HTML(http.StatusOK, "grammar", gin.H{ "hello": "Hello World", }) })

在上面渲染出来源码有很多空行。go在非动态数据上渲染是所有文字、空白和换行符都会保留。

在动态内容解析上使用 {{--}} 可以去除前后面空白或者换行符。对于动态内容又分为两种情况

  1. 变量等使用,前后面有空格的 {{ .hello }}
  2. 调用模板、循环等定义的 {{ template "name" pipeline? }} 这一行,会渲染空白行

对上面代码做简单修改,去除空白内容以及空白行:

规则:

建议:去除空白行,可以形成类似代码压缩效果,减少网络传输。对于浏览器来说影响几乎不计。

html
copy code
{{/* template/grammar */}} {{ define "grammar" -}} {{ template "header" . }} <p>由我来组成身体 {{- .hello -}} 。</p> {{ template "footer" . }} {{- end }}
html
copy code
{{/* template/footer */}} {{ define "footer" -}} <p>{{ .hello }}我来组成底部</p> </body> </html> {{- end }}
html
copy code
{{/* template/header */}} {{ define "header" -}} <!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> </head> <body> <p>{{ .hello }}我来组成头部</p> {{- end }}

模板中 {{/* comments */}} 这样书写注释

在模板中,Pipelines 的概念是传递数据,可能是链状的,也可能是一个函数的参数。只要能产生数据都属于 Pipelines。

以下几种示例都会生成 “output”

html
copy code
{{/* template/grammar */}} ...... <p> {{"\"output\""}} 字符串常量 <p> {{`"output"`}} 原始字符串常量 </p> <p> {{printf "%q" "output"}} 函数调用 </p> <p> {{"output" | printf "%q"}} 函数调用,最后一个参数来自前一个command的返回值 </p> <p> {{printf "%q" (print "out" "put")}} 加括号的参数 </p> <p> {{"put" | printf "%s%s" "out" | printf "%q"}} 玩出花的管道的链式调用 </p> <p> {{"output" | printf "%s" | printf "%q"}} 管道的链式调用 </p>

在go中使用 $name := pipelines 语法来定义一个变量,通常在循环中使用。

  • $ 是一个特殊变量,代指全局作用域对象。在无其他作用域 $ == . dot
  • 声明的变量仅在当前作用域有效
  • 内层作用域可以访问外层变量,外层不可以访问内层变量
html
copy code
{{/* template/grammar */}} ...... {{ range $value, $key := $}} <p>{{$key}} - {{$value}}</p> <p>{{ . }}</p> <p>{{ $ }}</p> {{ end }} ......

有两种语法可以遍历 mapslicearraychannel 等类型。

还提供了 {{ break }}{{ continue }} 来跳出循环和跳过本次循环。

第二种语法: 如果 pipeline 为真执行 T1 代码块,否则执行 T0 代码块。相当于加了个if判断。

html
copy code
{{/* template/grammar */}} ...... {{ range $value := .map}} <p>{{$value}}</p> <p>. = {{ . }}</p> <p>$ = {{ $ }}</p> {{ end }} {{ range $key, $value := .map}} <p>{{$key}} - {{$value}}</p> {{ end }} {{ range $value := .slice}} <p>{{$value}}</p> {{ end }} {{ range $key, $value := .slice}} {{ if eq $key 1 }} {{ continue }} {{ end }} <p>{{$key}} - {{$value}}</p> <p>. = {{ . }}</p> {{ end }} ......
go
copy code
/* main.go */ "map": map[string]string{"key1": "value1", "key2": "value2"}, "slice": []string{"slice1", "slice2", "slice3"},

执行模板时,函数从两个函数字典中查找:首先是模板函数字典,然后是全局函数字典。一般不在模板内定义函数,而是使用Funcs方法添加函数到模板里。.Method [Argument...]

函数可以有1~2个返回值,如果有第二个返回值,那么必须为 error 接口类型。如果第二个返回了非 nil 类型那么模板执行会中断并返回给调用者错误。

函数或者方法支持链式调用,.Field1.Key1.Method1.Field2.Key2.Method2,深度不限。

go
copy code
/* main.go */ func add(x, y int) int { return x + y } r.SetFuncMap(template.FuncMap{ "add": add, }) r.LoadHTMLGlob("template/*")
html
copy code
{{/* template/grammar */}} ...... <p>1 + 2 = {{ add 1 2 }}</p>
text
copy code
and 函数返回它的第一个empty参数或者最后一个参数; 就是说"and x y"等价于"if x then y else x";所有参数都会执行; or 返回第一个非empty参数或者最后一个参数; 亦即"or x y"等价于"if x then x else y";所有参数都会执行; not 返回它的单个参数的布尔值的否定 len 返回它的参数的整数类型长度 index 执行结果为第一个参数以剩下的参数为索引/键指向的值; 如"index x 1 2 3"返回x[1][2][3]的值;每个被索引的主体必须是数组、切片或者字典。 print 即fmt.Sprint printf 即fmt.Sprintf println 即fmt.Sprintln html 返回其参数文本表示的HTML逸码等价表示。 urlquery 返回其参数文本表示的可嵌入URL查询的逸码等价表示。 js 返回其参数文本表示的JavaScript逸码等价表示。 call 执行结果是调用第一个参数的返回值,该参数必须是函数类型,其余参数作为调用该函数的参数; 如"call .X.Y 1 2"等价于go语言里的dot.X.Y(1, 2); 其中Y是函数类型的字段或者字典的值,或者其他类似情况; call的第一个参数的执行结果必须是函数类型的值(和预定义函数如print明显不同); 该函数类型值必须有1到2个返回值,如果有2个则后一个必须是error接口类型; 如果有2个返回值的方法返回的error非nil,模板执行会中断并返回给调用模板执行者该错误; 布尔函数会将任何类型的零值视为假,其余视为真。 eq 如果arg1 == arg2则返回真 ne 如果arg1 != arg2则返回真 lt 如果arg1 < arg2则返回真 le 如果arg1 <= arg2则返回真 gt 如果arg1 > arg2则返回真 ge 如果arg1 >= arg2则返回真

pipeline 判断条件为各数据类型的零值。例如

text
copy code
{{if pipeline}} T1 {{end}} 如果pipeline的值为empty,不产生输出,否则输出T1执行结果。不改变dot的值。 Empty值包括false、0、任意nil指针或者nil接口,任意长度为0的数组、切片、字典。 {{if pipeline}} T1 {{else}} T0 {{end}} 如果pipeline的值为empty,输出T0执行结果,否则输出T1执行结果。不改变dot的值。 {{if pipeline}} T1 {{else if pipeline}} T0 {{end}} 用于简化if-else链条,else action可以直接包含另一个if;等价于: {{if pipeline}} T1 {{else}}{{if pipeline}} T0 {{end}}{{end}}

with 用来将 . 值指向 pipeline 。

第二种语法也是加了 if 的语法糖格式。

text
copy code
{{with pipeline}} T1 {{end}} {{with pipeline}} T1 {{else}} T0 {{end}}

我的理解为局部定义模板。

新建一个 component.gohtml 模板文件,在 grammar.gohtml 中调用这个模板文件,如果 block 定义的名称等于调用这个文件名称则使用 block 的内容,否则使用原本的模板文件。

html
copy code
{{/* template/component */}} {{ define "component" }} <p>我是定义的组件 {{ .hello }}</p> {{ end }}
html
copy code
{{/* template/component */}} <!-- 如果block "component1",则这里显示原本模板内容 --> <!-- 否则显示 block 定义的内容 --> {{ template "component"}} {{ block "component1" . }} <p>我是 block 默认内容</p> {{ end }}

发表您的看法

评论 (0 条)