Golang html/template包


Template 模板引擎 将模板和数据进行渲染 输出格式化后的字符程序
golang提供 两个 Template 标准库
text/template $GOROOT/src/text/template
html/template $GOROOT/src/html/template

html/template格式化html字符

go 模板引擎三步
创建模板对象
加载模板字串
执行渲染模板//把加载的字符和数据进行格式化
go提供 标准库html/template 处理模板的接口
├── main.go
└── templates
    ├── index.html
    └── layout.html
layout.html文件
<!DOCTYPE html><html><head><meta charset="utf-8"><title>layout</title></head>
<body><h3>This is layout</h3>template data: {{ . }}</body></html>
ParseFiles 加载模板 该方法 返回模板对象和错误  用模板对象执行模板 注入数据对象
go 提供 模板标签 称之为 *action* 是 action
func templateHandler(w http.ResponseWriter, r *http.Request){
    t, _ :=template.ParseFiles("templates/layout.html")
    fmt.Println(t.Name())
    t.Execute(w, "Hello world")
}
打印t模板对象的Name方法 每一个模板 都有一个名字
如果不显示指定这个名字 go会把文件名(包括扩展名当成名字)
本例则是layout.html 访问之后可以看见返回的html字串
curl -i http://127.0.0.1:8000/
go 解析模板文件 也可解析模板字串 标准的处理 新建-加载-执行三部曲
func templateHandler(w http.ResponseWriter, r *http.Request){
    tmpl := `<!DOCTYPE html><html><head><meta charset="utf-8"> <title>Go Web Programming</title></head>
    <body>{{ . }}</body></html>`
    t := template.New("layout.html")
    t, _ = t.Parse(tmpl)
    fmt.Println(t.Name())
    t.Execute(w, "Hello World")
}
开发最终页面 可能是多个模板文件的嵌套
go的ParseFiles支持加载多个模板文件 不过模板对象的名字则是第一个模板文件的文件名
func templateHandler(w http.ResponseWriter, r *http.Request){
    t, _ :=template.ParseFiles("templates/layout.html", "templates/index.html")
    fmt.Println(t.Name())
    t.Execute(w, "Hello world")
}ParseGlob 方法 通过glob通配符加载模板

模板命名与嵌套

模板对象有名字  创建模板对象时 显示命名

可让go自动命名 涉及到嵌套模板的时候 如何命名模板
go提供 ExecuteTemplate 方法 用于执行指定名字的模板 如加载 layout.html 模板 可以指定layout.html
func templateHandler(w http.ResponseWriter, r *http.Request){
    t, _ :=template.ParseFiles("templates/layout.html")
    fmt.Println(t.Name())
    t.ExecuteTemplate(w, "layout", "Hello world")
}修改layout.html文件
{{ define "layout" }}<!DOCTYPE html><html><head><meta charset="utf-8"><title>layout</title></head>
<body><h3>This is layout</h3>template data: {{ . }}</body></html>{{ end }}
模板文件中 使用 define 这个action给模板文件命名 虽然 ParseFiles 方法返回的模板对象 t 名字还是layout.html
但 ExecuteTemplate 执行的模板 是html文件中定义的 layout
通过 define 定义模板 可通过 template action 引入模板  修改 layout.html 和 index.html
{{ define "layout" }}<!DOCTYPE html><html><head><meta charset="utf-8"><title>layout</title></head>
<body><h3>This is layout</h3>template data: {{ . }}{{ template "index" }}</body></html>{{ end }}
index.html 模板
{{ define "index" }}<div style="background: yellow">this is index.html</div>{{ end }}
用 ParseFiles 加载需要渲染的模板文件
func templateHandler(w http.ResponseWriter, r *http.Request){
    t, _ :=template.ParseFiles("templates/layout.html", "templates/index.html")
    t.ExecuteTemplate(w, "layout", "Hello world")
} 访问看到 index 被 layout 模板include了 curl http://127.0.0.1:8000/

单文件嵌套

创建模板对象 和 加载多个模板文件 执行模板文件时 需要指定 base模板 layout
base模板中 include 其他命名的模板 无论点 .  define template 花括号包裹的东西都是go的action(模板标签)
Action
action是go模板 用于动态执行 逻辑和展示数据的形式
条件语句 迭代 封装

模板条件判断

{{ if arg }}some content{{ end }}
{{ if arg }}some content{{ else }}other content{{ end }}
arg  是基本数据结构 也可以是表达式
if-end包裹的内容为条件为真的时候展示 与if语句一样 模板也可以有else语句
func templateHandler(w http.ResponseWriter, r *http.Request){
    t, _ :=template.ParseFiles("templates/layout.html")
    rand.Seed(time.Now().Unix())
    t.ExecuteTemplate(w, "layout", rand.Intn(10) > 5)
}
{{ define "layout" }}<!DOCTYPE html><html><head><meta charset="utf-8"><title>layout</title></head><body>
    <h3>This is layout</h3>template data: {{ . }}{{ if . }}    Number is greater than 5!{{ else }}    Number is 5 or less!{{ end }}</body></html>{{ end }}
当. 值为true 显示if 逻辑 否则显示else的逻辑

模板迭代

数组 切片或 map  使用迭代的action
与go的迭代类似 使用range处理
func templateHandler(w http.ResponseWriter, r *http.Request) {
    t := template.Must(template.ParseFiles("templates/layout.html"))
    daysOfWeek := []string{"Mon", "Tue", "Wed", "Ths", "Fri", "Sat", "Sun"}
    t.ExecuteTemplate(w, "layout", daysOfWeek)
}
{{ define "layout" }}
<!DOCTYPE html><html><head><meta charset="utf-8"><title>layout</title></head>
<body><h3>This is layout</h3>template data: {{ . }}{{ range . }}<li>{{ . }}</li>{{ end }}</body></html>{{ end }}
输出li列表 迭代的时候 还可以使用$设置循环变量
{{ range $key, $value := . }}    <li>key: {{ $key }}, value: {{ $value }}</li>{{ else }}empty{{ end }}
和迭代切片很像 rang也可以使用else语句
func templateHandler(w http.ResponseWriter, r *http.Request) {
    t := template.Must(template.ParseFiles("templates/layout.html"))
    daysOfWeek := []string{}
    t.ExecuteTemplate(w, "layout", daysOfWeek)
}
{{ range . }}<li>{{ . }}</li>{{ else }}empty{{ end }}
当range的结构为空的时候 则会执行else分支的逻辑

模板with封装

go模板 with语句 是创建一个封闭的作用域 在其范围内 可以使用.action 而与外面的.无关 只与with的参数有关
{{ with arg }}此时的点 . 就是arg{{ end }}
{{ define "layout" }}<!DOCTYPE html><html><head><meta charset="utf-8"><title>layout</title></head>
<body><h3>This is layout</h3>template data: {{ . }} {{ with "world"}}    Now the dot is set to {{ . }} {{ end }}</body></html>{{ end }}
访问结果curl http://127.0.0.1:8000/
template data: [Mon Tue Wed Ths Fri Sat Sun]
Now the dot is set to world
with语句的.与其外面的.是两个不相关的对象 with语句也可以有else
else中的.则和with外面的.一样 毕竟只有with语句内才有封闭的上下文
{{ with ""}}Now the dot is set to {{ . }}{{ else }}{{ . }}{{ end }}
访问效果curl http://127.0.0.1:8000/
template data: [Mon Tue Wed Ths Fri Sat Sun]
[Mon Tue Wed Ths Fri Sat Sun]

模板引用

func templateHandler(w http.ResponseWriter, r *http.Request) {
    t := template.Must(template.ParseFiles("templates/layout.html", "templates/index.html"))
    daysOfWeek := []string{"Mon", "Tue", "Wed", "Ths", "Fri", "Sat", "Sun"}
    t.ExecuteTemplate(w, "layout", daysOfWeek)
}
layout中引用了 index模板
{{ define "layout" }}<!DOCTYPE html><html><head><meta charset="utf-8"><title>layout</title></head><body>
<h3>This is layout</h3>layout template data: ({{ . }}){{ template "index" }}</body></html>{{ end }}

index.html 内容打印 .
{{ define "index" }}<div style="background: yellow">this is index.html ({{ . }})</div>{{ end }}
访问的效果 index.html 中的点并没有数据
curl http://127.0.0.1:8000/
<h3>This is layout</h3>
layout template data: ([Mon Tue Wed Ths Fri Sat Sun])
<div style="background: yellow">this is index.html ()</div>
修改引用语句{{ template "index" . }} 把参数传给子模板 就能看见index.html模板 有数据
<div style="background: yellow">this is index.html ([Mon Tue Wed Ths Fri Sat Sun])</div

参数 变量和管道

模板的参数可以是go中的基本数据类型 如字串 数字 布尔值 数组切片或者一个结构体 在模板中设置变量可以使用 $variable := value
在range迭代的过程使用了设置变量的方式
go 模板的管道函数  通过定义函数过滤器 实现模板的一些简单格式化处理 并且通过管道哲学 这样的处理方式可以连成一起
{{ p1 | p2 | p3 }} 如 模板内置了函数格式化输出 {{ 12.3456 | printf "%.2f" }}

模板函数

管道符可以成为模板中的过滤器 除了内建的函数 能够自定义函数可以扩展模板的功能
go模板提供了自定义模板函数的功能
想要创建一个定义函数需要两步
创建 FuncMap 类型的map key是模板函数的名字 value是其函数的定义
将 FuncMap 注入到模板中
func templateHandler(w http.ResponseWriter, r *http.Request) {
    funcMap := template.FuncMap{"fdate": formDate}
    t := template.New("layout").Funcs(funcMap)
    t = template.Must(t.ParseFiles("templates/layout.html", "templates/index.html"))
    t.ExecuteTemplate(w, "layout", time.Now())
}在模板中使用{{ . | fdate }} 也可以不用管道过滤器 而是使用正常的函数调用形式 {{ fdate . }}
函数的注入 必须要在 parseFiles 之前 因为解析模板的时候 需要先把函数编译注入

模板智能上下文

go 根据上下文显示模板的内容 如字符的转义 根据所显示的上下文环境而智能变化 比如同样的html标签 在Js和html环境中 其转义的内容是不一样
func templateHandler(w http.ResponseWriter, r *http.Request){
    t, _ :=template.ParseFiles("templates/layout.html")
    content := `I asked: <i>What's up?</i>`
    t.ExecuteTemplate(w, "layout", content)
}模板文件
{{ define "layout" }}<!DOCTYPE html><html><head><meta charset="utf-8"><title>layout</title></head>
<body><h3>This is layout</h3>layout template data: ({{ . }})
    <div><a href="/{{ . }}">Path</a></div>
    <div><a href="/?q={{ . }}">Query</a></div>
    <div><a onclick="f('{{ . }}')">Onclick</a></div>
</body></html>{{ end }}
访问结果
layout template data: (I asked: <i>What's up?</i>)
<div><a href="/I%20asked:%20%3ci%3eWhat%27s%20up?%3c/i%3e">Path</a></div>
<div><a href="/?q=I%20asked%3a%20%3ci%3eWhat%27s%20up%3f%3c%2fi%3e">Query</a></div>
<div><a onclick="f('I asked: \x3ci\x3eWhat\x27s up?\x3c\/i\x3e')">Onclick</a></div>

模板安全

go自动处理html标签的转义 对web安全具有重要作用 避免了一些XSS攻击
XSS安全
安全是一个很大的话题 XSS安全包含很多内容
XSS主要分为三种
通过提交待script标签的内容执行js
layout.html加一个表单
<form action="/" method="post">Comment: <input name="comment" type="text"><hr/><button id="submit">Submit</button></form>
go的表单 处理函数
func templateHandler(w http.ResponseWriter, r *http.Request){
    t, _ :=template.ParseFiles("templates/layout.html")
    t.ExecuteTemplate(w, "layout", r.FormValue("comment"))
}
提交一段js 到go在表达处理的时候 自动做了xss过滤
如果不想转义标签 需要使用template.HTML方法包裹
func templateHandler(w http.ResponseWriter, r *http.Request){
w.Header().Set("X-XSS-Protection", "0")
t, _ :=template.ParseFiles("templates/layout.html")
t.ExecuteTemplate(w, "layout", template.HTML(r.FormValue("comment")))
}

模板自定义函数

golang 模板template自定义函数用法
package main
import (
    "html/template"
    "net/http"
    "time"
)
type User struct {
    Username, Password string
    RegTime            time.Time
}
//函数名字大写 不然模板 无法调用
func ShowTime(t time.Time, format string) string {
 return t.Format(format)
}
func main() {
  http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    //template.New 参数名字要和 ParseFiles 函数的文件名要相同 不然报错 "" is an incomplete template
    tmpl := template.New("demo.html")
    tmpl = tmpl.Funcs(template.FuncMap{"showtime": ShowTime})
    tmpl, _ = tmpl.ParseFiles("demo.html")
    //mpl, _ = tmpl.Parse(`<p>{{.Username}}|{{.Password}}|{{.RegTime.Format "2006-01-02 15:04:05"}}</p>
    //<p>{{.Username}}|{{.Password}}|{{showtime .RegTime "2006-01-02 15:04:05"}}</p>
    //`)
    // tmpl = template.Must(tmpl.ParseFiles("templates/demo.html"))  // 这样写 这个template.Must到底干嘛的呢 就是省略了一个err
    user := User{"username", "password", time.Now()}
    if err := tmpl.ExecuteTemplate(w, "demo.html", user); nil != err {
        http.Error(w, err.Error(), http.StatusBadRequest)
    }
})
 http.ListenAndServe(":8082", nil)
}
<h1>Func</h1>
<p>{{.Username}}|{{.Password}}|{{.RegTime.Format "2006-01-02 15:04:05"}}</p>
<p>{{.Username}}|{{.Password}}|{{showtime .RegTime "2006-01-02 15:04:05"}}</p>

Must 函数

// Must is a helper that wraps a call to a function returning (*Template, error)
// and panics if the error is non-nil. It is intended for use in variable initializations
// such as     var t = template.Must(template.New("name").Parse("html"))
func Must(t *Template, err error) *Template {
  if err != nil {
        panic(err)
  }
  return t
}用于验证模板 如大括号 注释和变量的匹配
package main
import (
    "fmt"
    "text/template"
)
func main() {
    t1 := template.New("first")
    template.Must(t1.Parse(" some static text "))
    fmt.Println("第一个解析OK")
t2 := template.New("second")
    template.Must(t2.Parse("some static text {{ .Name }}"))
    fmt.Println("第二个解析OK")
    fmt.Println("下一个解析失败")
    t3 := template.New("check parse error with Must")
    template.Must(t3.Parse(" some static text {{ .Name }"))//panic: template: check parse error with Must:1: unexpected "}" in operand
}

从字符串载入模板

tplStr := `{{ .Name }} {{ .Age }}`
tpl := template.Must(template.New("tplName").Parse(tplStr))
tpl.Execute(os.Stdout, map[string]interface{}{Name: "big_cat", Age: 29})

从文件载入模板

模板文件 每个模板文件显式 定义模板名称 {{ define "tplName" }}  模板对象名 与 模板名不一致 无法解析
用 {{ define "tplName" }} 定义模板名
用 {{ template "tplName" . }} 引入其他模板
用 . 访问当前数据域 比如 range 里使用 . 访问 循环项的数据域
用 $. 访问绝对顶层数据域
views/header.html
{{ define "header" }}<!doctype html><head><meta charset="UTF-8"><title>{{ .PageTitle }}</title></head>{{ end }}
views/footer.html
{{ define "footer" }}</html>{{ end }}
views/index/index.html    
{{ define "index/index" }} {{}}{{ template "header" . }}
<body><div>hello, {{ .Name }}, age {{ .Age }}</div></body> {{ template "footer" . }}{{ end }}
views/news/index.html
{{ define "news/index" }}
  {{ template "header" . }}
  <body>{{}}
  {{ $pageTitle := "news title" }}
  {{ $pageTitleLen := len $pageTitle }}
  {{}}
  {{ if gt $pageTitleLen 4 }}<h4>{{ $pageTitle }}</h4>{{ end }}
  {{ $c1 := gt 4 3}}
  {{ $c2 := lt 2 3 }}
  {{}}
  {{ if and $c1 $c2 }}<h4>1 == 1 3 > 2 4 < 5</h4>{{ end }}
  <div><ul>
      {{ range .List }}
        {{ $title := .Title }}{{}}
        <li>{{ $title }} - {{ .CreatedAt.Format "2006-01-02 15:04:05" }} - Author {{ $.Author }}</li>
      {{end}}
    </ul>
    {{}}
    {{ with .Total }}<div>总数:{{ . }}</div>{{ end }}
  </div>
  </body>
  {{ template "footer" . }}
{{ end }}

template.ParseFiles

手动定义 要载入的模板 解析后制定需要渲染的模板名 news/index
// 从模板文件构建
tpl := template.Must(
  template.ParseFiles("views/index/index.html","views/news/index.html","views/header.html","views/footer.html",),
)// render template with tplName index
_ = tpl.ExecuteTemplate(os.Stdout,"index/index",map[string]interface{}{PageTitle: "首页",Name: "cat",Age: 29, },)
// render template with tplName index
_ = tpl.ExecuteTemplate(os.Stdout,"news/index",map[string]interface{}{"PageTitle": "新闻","List": []struct {
      Title   string
      CreatedAt time.Time
    }{{Title: "golang views/template example", CreatedAt: time.Now()},
      {Title: "be honest, i don't very like this raw engine", CreatedAt: time.Now()},
    },
    "Total": 1,
    "Author": "big_cat",
  },
)

template.ParseGlob

手动 指定每一个模板文件 在一些场景下难免难以满足需求 可以使用通配符正则匹配载入
正则不包含文件夹 否则会因文件夹被作为视图载入无法解析而报错 可以设定多个模式串 如下载入了一级目录和二级目录的视图文件
// 从模板文件构建 tpl := template.Must(template.ParseGlob("views*.html"))

Web服务器

package main
import (
  "html/template"
  "log"
  "net/http"
  "time"
)
var (
  htmlTplEngine  *template.Template
  htmlTplEngineErr error
)
func init() {
  // 初始化模板引擎 并加载各层级的模板文件
  // views*.html 来加载 view 下的各子目录中的模板文件
  htmlTplEngine = template.New("htmlTplEngine")// 模板根目录下的模板文件 一些公共文件
  _, htmlTplEngineErr = htmlTplEngine.ParseGlob("views*.html")
  if nil != htmlTplEngineErr {
    log.Panic(htmlTplEngineErr.Error())
  }
}
func IndexHandler(w http.ResponseWriter, r *http.Request) {
  _ = htmlTplEngine.ExecuteTemplate(w,"index/index",
    map[string]interface{}{"PageTitle": "首页", "Name": "sqrt_cat", "Age": 25},
  )
}// news
func NewsHandler(w http.ResponseWriter, r *http.Request) {
  _ = htmlTplEngine.ExecuteTemplate(w,"news/index", map[string]interface{}{
      "PageTitle": "新闻",
      "List": []struct {
        Title   string
        CreatedAt time.Time
      }{{Title: "this is golang views/template example", CreatedAt: time.Now()},
        {Title: "to be honest, i don't very like this raw engine", CreatedAt: time.Now()},
      },
      "Total": 1,
      "Author": "big_cat",
    },
  )
}
func main() {
  http.HandleFunc("/", IndexHandler)
  http.HandleFunc("/index", IndexHandler)
  http.HandleFunc("/news", NewsHandler)
  serverErr := http.ListenAndServe(":8085", nil)
  if nil != serverErr {
    log.Panic(serverErr.Error())
  }
}
板对象有名字 如果没有定义名字 会用第一个被载入的视图文件 baseName 做名
template.ParseFiles/template.ParseGlob 生成模板对象时 没指定模板对象名 则用第一个被载入的文件 如 index/index.html 的 baseName 即 index.html 做默认名
如果 tplObj.Execute 方法执行渲染 会查找名为 index.html 的模板 所以常用 tplObj.ExecuteTemplate 自己指定要渲染的模板名

Golang html template和html-template相关