Golang 的模版 template 源碼分析
golang 的模版使用分兩步
1,模版解析
tpl, err := template.Parse(filename) 得到文件名爲名字的模板,並保存在 tpl 變量中
template.ParseFiles(filenames) 可以解析一組模板
template.ParseGlob(pattern) 會根據 pattern 解析所有匹配的模板並保存
2,數據綁定
tpl.Execute(io.Writer, data) 去執行, 模板渲染後的內容寫入到 io.Writer 中。Data 是傳給模板的動態數據。
tpl.ExecuteTemplate(io.Writer, name, data) 和上面的簡單模板類似,只不過傳入了一個模板的名字,指定要渲染的模板 (因爲 tpl 可以包含多個模板
嵌套模板定義
嵌套模板定義如下:
{ {define "footer"}}
<footer>
<p>Here is the footer</p>
</footer>
{ {end}}
其他模板中使用:
{ {template "footer"}}
模版的數據綁定也有兩種方式
1,模版變量
通過. 獲取變量
通過{ { .FieldName }}
來訪問它的字段
鏈式訪問 { { .Struct.StructTwo.Field }}
2,函數變量
2.1,調用結構體的方法
{ {if .User.HasPermission "feature-a"}}
2.2 調用結構體的函數變量(屬性是一個函數)
type User struct {
ID int
Email string
HasPermission func(string) bool
}
{ {if (call .User.HasPermission "feature-b")}}
2.3 使用 template.FuncMap 創建自定義的函數
FuncMap 通過 map[string]interface{} 將函數名映射到函數上。注意映射的函數必須只有一個返回值,或者有兩個返回值但是第二個是 error 類型。
testTemplate, err = template.New("hello.gohtml").Funcs(template.FuncMap{
"hasPermission": func(user User, feature string) bool {
if user.ID == 1 && feature == "feature-a" {
return true
}
return false
},
}).ParseFiles("hello.gohtml")
{ { if hasPermission .User "feature-a" }}
2.4 第三方自定義函數
比如 sprig 庫
由於函數變量的方式綁定數據可以使用閉包,所以數據的綁定時機有兩個
1,使用閉包
template.New("hello.gohtml").Funcs(template.FuncMap
2,Execute 數據綁定
tpl.Execute(io.Writer, data)
常用的兩個模版包
"text/template"
template 包實現了數據驅動的用於生成文本輸出的模板。其實簡單來說就是將一組文本嵌入另一組文本模版中,返回一個你期望的文本
"html/template"
生成 HTML 格式的輸出,該包提供了相同的接口,但會自動將輸出轉化爲安全的 HTML 格式輸出,可以抵抗一些網絡攻擊。 內部其實是用了 "text/template"
看下這兩個包的源碼目錄
text/template
cd /usr/local/go/src/text/template
tree
.
|____option.go
|____examplefunc_test.go
|____testdata
| |____file1.tmpl
| |____tmpl1.tmpl
| |____tmpl2.tmpl
| |____file2.tmpl
|____examplefiles_test.go
|____example_test.go
|____exec_test.go
|____link_test.go
|____multi_test.go
|____parse
| |____lex_test.go
| |____lex.go
| |____parse_test.go
| |____node.go
| |____parse.go
|____exec.go
|____template.go
|____helper.go
|____doc.go
|____funcs.go
html/template
cd /usr/local/go/src/html/template
tree
.
|____testdata
| |____file1.tmpl
| |____tmpl1.tmpl
| |____fs.zip
| |____tmpl2.tmpl
| |____file2.tmpl
|____clone_test.go
|____error.go
|____examplefiles_test.go
|____example_test.go
|____content_test.go
|____escape.go
|____exec_test.go
|____transition_test.go
|____multi_test.go
|____js_test.go
|____element_string.go
|____urlpart_string.go
|____transition.go
|____css_test.go
|____template_test.go
|____html.go
|____state_string.go
|____js.go
|____html_test.go
|____delim_string.go
|____template.go
|____doc.go
|____context.go
|____attr_string.go
|____content.go
|____css.go
|____url.go
|____escape_test.go
|____attr.go
|____url_test.go
|____jsctx_string.go
下面簡單介紹下 text/template
它的工作流程大概分爲下面幾步,
通過 lex 進行詞法分析 =>parse 解析語法樹 =>execute 渲染生成目標文件
首先看 Template 結構:
type Template struct {
name string
*parse.Tree
*common
leftDelim string
rightDelim string
}
name 是這個 Template 的名稱,Tree 是解析樹,common 是另一個結構,leftDelim 和 rightDelim 是左右兩邊的分隔符,默認爲 {{和}}。
type common struct {
tmpl map[string]*Template // Map from name to defined templates.
option option
muFuncs sync.RWMutex // protects parseFuncs and execFuncs
parseFuncs FuncMap
execFuncs map[string]reflect.Value
}
這個結構的第一個字段 tmpl 是一個 Template 的 map 結構,key 爲 template 的 name,value 爲 Template。也就是說,一個 common 結構
中可以包含多個 Template,而 Template 結構中又指向了一個 common 結構。所以,common 是一個模板組,在這個模板組中的 (tmpl 字段) 所有 Template 都共享一個 common(模板組),模板組中包含 parseFuncs 和 execFuncs。
使用 template.New() 函數可以創建一個空的、無解析數據的模板,同時還會創建一個 common,也就是模板組。
func New(name string) *Template {
t := &Template{
name: name,
}
t.init()
return t
}
其中 t 爲模板的關聯名稱,name 爲模板的名稱,t.init() 表示如果模板對象 t 還沒有 common 結構,就構造一個新的 common 組:
func (t *Template) init() {
if t.common == nil {
c := new(common)
c.tmpl = make(map[string]*Template)
c.parseFuncs = make(FuncMap)
c.execFuncs = make(map[string]reflect.Value)
t.common = c
}
}
新創建的 common 是空的,只有進行模板解析 (Parse(),ParseFiles() 等操作)之後,纔會將模板添加到 common 的 tmpl 字段 (map 結構) 中。
在執行了 Parse() 方法後,將會把模板 name 添加到 common tmpl 字段的 map 結構中,其中模板 name 爲 map 的 key,模板爲 map 的 value。
除了 template.New() 函數,還有一個 Template.New() 方法:
func (t *Template) New(name string) *Template {
t.init()
nt := &Template{
name: name,
common: t.common,
leftDelim: t.leftDelim,
rightDelim: t.rightDelim,
}
return nt
}
通過調用 t.New() 方法,可以創建一個新的名爲 name 的模板對象,並將此對象加入到 t 模板組中。
Execute() 和 ExecuteTemplate()
這兩個方法都可以用來應用已經解析好的模板,應用表示對需要評估的數據進行操作,並和無需評估數據進行合併,然後輸出 > 到 io.Writer 中:
func (t *Template) Execute(wr io.Writer, data interface{}) error
func (t *Template) ExecuteTemplate(wr io.Writer, name string, data interface{}) error
兩者的區別在於 Execute() 是應用整個 common 中已定義的模板對象,而 ExecuteTemplate() 可以選擇 common 中某個已定義的模板進行應用。
FuncMap 和 Funcs()
template 內置了一系列函數,但這些函數畢竟有限,可能無法滿足特殊的需求。template 允許我們定義自己的函數,添加到 common 中,然後就可以在待解析的內容中像使用內置函數一樣使用自定義的函數。
下面看看模版解析的 過程
parse.Parse 函數把文本解析成 map[string]*parse.Tree 的樹 map 對象, 然後把它 append 到當前模板的 t.temp 中
func (t *Tree) Parse(text, leftDelim, rightDelim string, treeSet map[string]*Tree, funcs ...map[string]interface{}) (tree *Tree, err error) {
defer t.recover(&err)
t.ParseName = t.Name
t.startParse(funcs, lex(t.Name, text, leftDelim, rightDelim), treeSet)
t.text = text
t.parse()
t.add()
t.stopParse()
return t, nil
}
// lex creates a new scanner for the input string.
func lex(name, input, left, right string) *lexer {
if left == "" {
left = leftDelim
}
if right == "" {
right = rightDelim
}
l := &lexer{
name: name,
input: input,
leftDelim: left,
rightDelim: right,
items: make(chan item),
line: 1,
}
go l.run()
return l
}
// run runs the state machine for the lexer.
func (l *lexer) run() {
for state := lexText; state != nil; {
state = state(l)
}
close(l.items)
}
// lexText scans until an opening action delimiter, "{{".
func lexText(l *lexer) stateFn {
l.width = 0
if x := strings.Index(l.input[l.pos:], l.leftDelim); x >= 0 {
ldn := Pos(len(l.leftDelim))
l.pos += Pos(x)
trimLength := Pos(0)
if strings.HasPrefix(l.input[l.pos+ldn:], leftTrimMarker) {
trimLength = rightTrimLength(l.input[l.start:l.pos])
}
l.pos -= trimLength
if l.pos > l.start {
l.emit(itemText)
}
l.pos += trimLength
l.ignore()
return lexLeftDelim
} else {
l.pos = Pos(len(l.input))
}
// Correctly reached EOF.
if l.pos > l.start {
l.emit(itemText)
}
l.emit(itemEOF)
return nil
}
下面看下數據綁定的過程,其實就是遍歷語法樹
// Walk functions step through the major pieces of the template structure,
// generating output as they go.
func (s *state) walk(dot reflect.Value, node parse.Node) {
s.at(node)
switch node := node.(type) {
case *parse.ActionNode:
// Do not pop variables so they persist until next end.
// Also, if the action declares variables, don't print the result.
val := s.evalPipeline(dot, node.Pipe)
if len(node.Pipe.Decl) == 0 {
s.printValue(node, val)
}
case *parse.IfNode:
s.walkIfOrWith(parse.NodeIf, dot, node.Pipe, node.List, node.ElseList)
case *parse.ListNode:
for _, node := range node.Nodes {
s.walk(dot, node)
}
case *parse.RangeNode:
s.walkRange(dot, node)
case *parse.TemplateNode:
s.walkTemplate(dot, node)
case *parse.TextNode:
if _, err := s.wr.Write(node.Text); err != nil {
s.writeError(err)
}
case *parse.WithNode:
s.walkIfOrWith(parse.NodeWith, dot, node.Pipe, node.List, node.ElseList)
default:
s.errorf("unknown node: %s", node)
}
}
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/yoe0QRC3S93FpmMXnkP7Vw