golang提供了两个标准库用来处理模版text/template和html/template。我们使用html/template格式化html字符。

模版引擎

模版引擎很多,Python的jinja,nodejs的jade等都很好。所谓模版引擎,则将模版和数据进行渲染的输出格式化后的字符程序。对于go,执行这个流程大概需要三步:

  • 创建模版对象

  • 加载模版子串

  • 执行渲染模版

其中最后一步就是把加载的字符和数据进行格式化。其过程可以总结下图:
Golang Template - 图1

go提供的标准库html/template提供了很多处理模版的接口,我们的项目结构为:
Golang Template - 图2
templates文件夹有两个文件,分别为模版文件。layout.html文件如下:

  1. !DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  5. <title>layout</title>
  6. </head>
  7. <body>
  8. <h3>This is layout</h3>
  9. template data: {{ . }}
  10. </body>
  11. </html>

我们可以使用ParseFiles方法加载模版,该方法会返回一个模版对象和错误,接下来就可以使用模版对象执行模版,注入数据对象。go提供了一些模版标签,称之为action,.也是一种action。

  1. func templateHandler(w http.ResponseWriter, r *http.Request){
  2. t, _ :=template.ParseFiles("templates/layout.html")
  3. fmt.Println(t.Name())
  4. t.Execute(w, "Hello world")
  5. }

我们打印了t模板对象的Name方法,实际上,每一个模板,都有一个名字,如果不显示指定这个名字,go将会把文件名(包括扩展名当成名字)本例则是layout.html。访问之后可以看见返回的html字串:

  1. curl -i http://127.0.0.1:8000/
  2. HTTP/1.1 200 OK
  3. Date: Fri, 09 Dec 2016 09:04:36 GMT
  4. Content-Length: 223
  5. Content-Type: text/html; charset=utf-8
  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  5. <title>layout</title>
  6. </head>
  7. <body>
  8. <h3>This is layout</h3>
  9. template data: Hello world
  10. </body>
  11. </html>

go不仅可以解析模版文件,也可以直接模版子串,这就是标准的处理,新建-加载-执行三部曲:

  1. func templateHandler(w http.ResponseWriter, r *http.Request){
  2. tmpl := `<!DOCTYPE html>
  3. <html>
  4. <head>
  5. <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>Go Web Programming</title>
  6. </head>
  7. <body>
  8. {{ . }}
  9. </body>
  10. </html>`
  11. t := template.New("layout.html")
  12. t, _ = t.Parse(tmpl)
  13. fmt.Println(t.Name())
  14. t.Execute(w, "Hello World")
  15. }

实际开发中,最终的页面很可能是多个模板文件的嵌套结果。go的ParseFiles也支持加载多个模板文件,不过模板对象的名字则是第一个模板文件的文件名。

  1. func templateHandler(w http.ResponseWriter, r *http.Request){
  2. t, _ :=template.ParseFiles("templates/layout.html", "templates/index.html")
  3. fmt.Println(t.Name())
  4. t.Execute(w, "Hello world")
  5. }

可见打印的还是 layout.html的名字,执行的模板的时候,并没有index.html的模板内容。此外,还有ParseGlob方法,可以通过glob通配符加载模板。

模版命名与嵌套

模版命名

模版对象是有名字的,可以在创建模版对象的时候显示命名,也可以让go自动命名。go提供了ExecuteTemplate方法,用于执行指定名字的模版。例如加载layout.html模版的时候,可以指定layout.html

  1. func templateHandler(w http.ResponseWriter, r *http.Request){
  2. t, _ :=template.ParseFiles("templates/layout.html")
  3. fmt.Println(t.Name())
  4. t.ExecuteTemplate(w, "layout", "Hello world")
  5. }

似乎和Execute方法没有太大的差别。下面修改一下layout.html文件:

  1. {{ define "layout" }}
  2. <!DOCTYPE html>
  3. <html>
  4. <head>
  5. <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  6. <title>layout</title>
  7. </head>
  8. <body>
  9. <h3>This is layout</h3>
  10. template data: {{ . }}
  11. </body>
  12. </html>
  13. {{ end }}

在模板文件中,使用了define这个action给模板文件命名了。虽然我们ParseFiles方法返回的模板对象t的名字还是layout.html, 但是ExecuteTemplate执行的模板却是html文件中定义的layout。

不仅可以通过define定义模板,还可以通过template action引入模板,类似jinja的include特性。修改 layout.html 和 index.html

  1. {{ define "layout" }}
  2. <!DOCTYPE html>
  3. <html>
  4. <head>
  5. <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  6. <title>layout</title>
  7. </head>
  8. <body>
  9. <h3>This is layout</h3>
  10. template data: {{ . }}
  11. {{ template "index" }}
  12. </body>
  13. </html>
  14. {{ end }}
  1. {{ define "index" }}
  2. <div style="background: yellow">
  3. this is index.html
  4. </div>
  5. {{ end }}

go的代码也需要修改,使用ParseFiles加载需要渲染的模版文件:

  1. func templateHandler(w http.ResponseWriter, r *http.Request){
  2. t, _ :=template.ParseFiles("templates/layout.html", "templates/index.html")
  3. t.ExecuteTemplate(w, "layout", "Hello world")
  4. }

访问可以看到index被layout模版include了:

  1. curl http://127.0.0.1:8000/
  2. <!DOCTYPE html>
  3. <html>
  4. <head>
  5. <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  6. <title>layout</title>
  7. </head>
  8. <body>
  9. <h3>This is layout</h3>
  10. template data: Hello world
  11. <div style="background: yellow">
  12. this is index.html
  13. </div>
  14. </body>
  15. </html>

单文件嵌套

总而言之,创建模板对象后和加载多个模板文件,执行模板文件的时候需要指定base模板(layout),在base模板中可以include其他命名的模板。无论点.,define,template这些花括号包裹的东西都是go的action(模板标签)。

Action

action是go模版中用于动态执行一些逻辑和展示数据的形式。大致分为下面几种:

  • 条件语句

  • 迭代

  • 封装

  • 引用

条件判断

条件判断的语法很简单:

  1. {{ if arg }}
  2. some content
  3. {{ end }}
  4. {{ if arg }}
  5. some content
  6. {{ else }}
  7. other content
  8. {{ end }}

arg可以是基本数据结构,也可以是表达式:if-end包裹的内容为条件为真的时候展示。和if语句一样,模版也可以有else语句。

  1. func templateHandler(w http.ResponseWriter, r *http.Request){
  2. t, _ :=template.ParseFiles("templates/layout.html")
  3. rand.Seed(time.Now().Unix())
  4. t.ExecuteTemplate(w, "layout", rand.Intn(10) > 5)
  5. }
  6. {{ define "layout" }}
  7. <!DOCTYPE html>
  8. <html>
  9. <head>
  10. <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  11. <title>layout</title>
  12. </head>
  13. <body>
  14. <h3>This is layout</h3>
  15. template data: {{ . }}
  16. {{ if . }}
  17. Number is greater than 5!
  18. {{ else }}
  19. Number is 5 or less!
  20. {{ end }}
  21. </body>
  22. </html>
  23. {{ end }}

此时就能看见,当.的值为true的时候显示if的逻辑,否则显示else的逻辑。

迭代

对于一些数组,切片或者是map,可以使用迭代的action,与go的迭代类似,使用range进行处理:

  1. func templateHandler(w http.ResponseWriter, r *http.Request) {
  2. t := template.Must(template.ParseFiles("templates/layout.html"))
  3. daysOfWeek := []string{"Mon", "Tue", "Wed", "Ths", "Fri", "Sat", "Sun"}
  4. t.ExecuteTemplate(w, "layout", daysOfWeek)
  5. }
  6. {{ define "layout" }}
  7. <!DOCTYPE html>
  8. <html>
  9. <head>
  10. <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  11. <title>layout</title>
  12. </head>
  13. <body>
  14. <h3>This is layout</h3>
  15. template data: {{ . }}
  16. {{ range . }}
  17. <li>{{ . }}</li>
  18. {{ end }}
  19. </body>
  20. </html>
  21. {{ end }}

可以看出输出了一堆li列表。迭代的时候,还可以使用$设置循环设置:

  1. {{ range $key, $value := . }}
  2. <li>key: {{ $key }}, value: {{ $value }}</li>
  3. {{ else }}
  4. empty
  5. {{ end }}

可以看见和迭代切片很像。rang也可以使用else语句:

  1. func templateHandler(w http.ResponseWriter, r *http.Request) {
  2. t := template.Must(template.ParseFiles("templates/layout.html"))
  3. daysOfWeek := []string{}
  4. t.ExecuteTemplate(w, "layout", daysOfWeek)
  5. }
  6. {{ range . }}
  7. <li>{{ . }}</li>
  8. {{ else }}
  9. empty
  10. {{ end }}

当range的结构为空的时候,则会执行else分支的逻辑。

with封装

with语言在Python中可以开启一个上下文环境。对于go模板,with语句类似,其含义就是创建一个封闭的作用域,在其范围内,可以使用.action,而与外面的.无关,只与with的参数有关:

  1. {{ with arg }}
  2. 此时的点 . 就是arg
  3. {{ end }}
  4. {{ define "layout" }}
  5. <!DOCTYPE html>
  6. <html>
  7. <head>
  8. <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  9. <title>layout</title>
  10. </head>
  11. <body>
  12. <h3>This is layout</h3>
  13. template data: {{ . }}
  14. {{ with "world"}}
  15. Now the dot is set to {{ . }}
  16. {{ end }}
  17. </body>
  18. </html>
  19. {{ end }}

访问结果如下:

  1. curl http://127.0.0.1:8000/
  2. <!DOCTYPE html>
  3. <html>
  4. <head>
  5. <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  6. <title>layout</title>
  7. </head>
  8. <body>
  9. <h3>This is layout</h3>
  10. template data: [Mon Tue Wed Ths Fri Sat Sun]
  11. Now the dot is set to world
  12. </body>
  13. </html>

可见 with语句的.与其外面的.是两个不相关的对象。with语句也可以有else。else中的.则和with外面的.一样,毕竟只有with语句内才有封闭的上下文:

  1. {{ with ""}}
  2. Now the dot is set to {{ . }}
  3. {{ else }}
  4. {{ . }}
  5. {{ end }}

访问效果为:

  1. curl http://127.0.0.1:8000/
  2. <!DOCTYPE html>
  3. <html>
  4. <head>
  5. <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  6. <title>layout</title>
  7. </head>
  8. <body>
  9. <h3>This is layout</h3>
  10. template data: [Mon Tue Wed Ths Fri Sat Sun]
  11. [Mon Tue Wed Ths Fri Sat Sun]
  12. </body>
  13. </html>

引用

我们已经介绍了模板嵌套引用的技巧。引用除了模板的include,还包括参数的传递。

  1. func templateHandler(w http.ResponseWriter, r *http.Request) {
  2. t := template.Must(template.ParseFiles("templates/layout.html", "templates/index.html"))
  3. daysOfWeek := []string{"Mon", "Tue", "Wed", "Ths", "Fri", "Sat", "Sun"}
  4. t.ExecuteTemplate(w, "layout", daysOfWeek)
  5. }

修改 layout.html, layout中引用了 index模板:

  1. {{ define "layout" }}
  2. <!DOCTYPE html>
  3. <html>
  4. <head>
  5. <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  6. <title>layout</title>
  7. </head>
  8. <body>
  9. <h3>This is layout</h3>
  10. layout template data: ({{ . }})
  11. {{ template "index" }}
  12. </body>
  13. </html>
  14. {{ end }}

index.html模板的内容也打印了 .:

  1. {{ define "index" }}
  2. <div style="background: yellow">
  3. this is index.html ({{ . }})
  4. </div>
  5. {{ end }}

访问的效果如下,index.html 中的点并没有数据。

  1. curl http://127.0.0.1:8000/
  2. <!DOCTYPE html>
  3. <html>
  4. <head>
  5. <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  6. <title>layout</title>
  7. </head>
  8. <body>
  9. <h3>This is layout</h3>
  10. layout template data: ([Mon Tue Wed Ths Fri Sat Sun])
  11. <div style="background: yellow">
  12. this is index.html ()
  13. </div>
  14. </body>
  15. </html>

我们可以修改引用语句{{ template “index” . }},把参数传给子模板,再次访问,就能看见index.html模板也有数据啦。

  1. <div style="background: yellow">
  2. this is index.html ([Mon Tue Wed Ths Fri Sat Sun])
  3. </div>

参数,变量和管道

模板的参数可以是go中的基本数据类型,如字串,数字,布尔值,数组切片或者一个结构体。在模板中设置变量可以使用 $variable := value。我们在range迭代的过程使用了设置变量的方式。

go还有一个特性就是模板的管道函数,熟悉django和jinja的开发者应该很熟悉这种手法。通过定义函数过滤器,实现模板的一些简单格式化处理。并且通过管道哲学,这样的处理方式可以连成一起。

  1. {{ p1 | p2 | p3 }}

例如 模板内置了一些函数,比如格式化输出:

  1. {{ 12.3456 | printf "%.2f" }}

函数

既然管道符可以成为模板中的过滤器,那么除了内建的函数,能够自定义函数可以扩展模板的功能。幸好go的模板提供了自定义模板函数的功能。

想要创建一个定义函数只需要两步:

  1. 创建一个FuncMap类型的map,key是模板函数的名字,value是其函数的定义。

  2. 将 FuncMap注入到模板中。

  1. func templateHandler(w http.ResponseWriter, r *http.Request) {
  2. funcMap := template.FuncMap{"fdate": formDate}
  3. t := template.New("layout").Funcs(funcMap)
  4. t = template.Must(t.ParseFiles("templates/layout.html", "templates/index.html"))
  5. t.ExecuteTemplate(w, "layout", time.Now())
  6. }

然后在模板中使用{{ . | fdate }},当然也可以不适用管道过滤器,而是使用正常的函数调用形式,{{ fdate . }}。
注意,函数的注入,必须要在parseFiles之前,因为解析模板的时候,需要先把函数编译注入。

智能上下文

go还提供了一个更有意思的特性。那就是根据上下文显示模板的内容。例如字符的转义,会根据所显示的上下文环境而智能变化。比如同样的html标签,在Js和html环境中,其转义的内容是不一样的:

  1. func templateHandler(w http.ResponseWriter, r *http.Request){
  2. t, _ :=template.ParseFiles("templates/layout.html")
  3. content := `I asked: <i>What's up?</i>`
  4. t.ExecuteTemplate(w, "layout", content)
  5. }

模板文件:

  1. {{ define "layout" }}
  2. <!DOCTYPE html>
  3. <html>
  4. <head>
  5. <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  6. <title>layout</title>
  7. </head>
  8. <body>
  9. <h3>This is layout</h3>
  10. layout template data: ({{ . }})
  11. <div><a href="/{{ . }}">Path</a></div>
  12. <div><a href="/?q={{ . }}">Query</a></div>
  13. <div><a onclick="f('{{ . }}')">Onclick</a></div>
  14. </body>
  15. </html>
  16. {{ end }}

访问结果

  1. layout template data: (I asked: <i>What's up?</i>)
  2. <div><a href="/I%20asked:%20%3ci%3eWhat%27s%20up?%3c/i%3e">Path</a></div>
  3. <div><a href="/?q=I%20asked%3a%20%3ci%3eWhat%27s%20up%3f%3c%2fi%3e">Query</a></div>
  4. <div><a onclick="f('I asked: \x3ci\x3eWhat\x27s up?\x3c\/i\x3e')">Onclick</a></div>

可以看见go会自动为我们处理html标签的转义。这对web安全具有重要作用。避免了一些XSS攻击。


Golang Template - 图3