文章目录

  • Go Template 简介
  • 模板的结构
    • Template 与 common 的结构
  • 模板函数和方法
    • New() 函数和 init() 方法
    • New() 方法
    • Parse() 方法
    • ParseFiles() 方法/函数和 ParseGlob() 方法/函数
    • Execute() 方法和 ExecuteTemplate() 方法
    • Lookup() 方法、DefinedTemplates() 方法和 Templates() 方法
    • Clone() 方法
    • Must() 函数
    • FuncMap 与 Funcs() 函数
  • 模板处理过程

Go Template 简介


模板可以理解为事先定义好的 HTML 文档文件,模板渲染的作用机制可以简单理解为文本替换操作(使用相应的数据去替换 HTML 文档中事先准备好的标记),模板用于显示和数据分离(前后端分离),模板技术本质是模板引擎利用模板文件和数据生成 HTML 文档。

Go 语言内置了文本模板引擎 text/template 包和用于 HTML 文档的 html/template 包,它们的作用机制可以简单归纳如下:

  • 模板文件通常定义为 .tmpl.tpl 为后缀(也可以使用其他的后缀),必须使用 UTF8 编码。

  • 模板文件中使用 {{}} 包裹和标识需要传入的数据。

  • 传给模板这样的数据就可以通过点号 . 来访问,如果数据是复杂类型的数据,可以通过 { { .FieldName }} 的方式来访问它的字段。

  • {{}} 包裹的内容外,其他内容均不做修改原样输出。

模板引擎的两个步骤:

(1)模板文件列表或者字符串进行语法分析,创建模板结构 。

(2)模板引擎结合模板结构和传入动态数据,执行生成 HTML ,传递给 ResponseWriter 接口。

Go 语言 template 包中不少函数、方法都直接返回 *Template(模板对象)类型,其函数和方法如下:

func Must(t *Template, err error) *Template
func New(name string) *Template
func ParseFS(fs fs.FS, patterns ...string) (*Template, error)
func ParseFiles(filenames ...string) (*Template, error)
func ParseGlob(pattern string) (*Template, error)
func (t *Template) AddParseTree(name string, tree *parse.Tree) (*Template, error)
func (t *Template) Clone() (*Template, error)
func (t *Template) DefinedTemplates() string
func (t *Template) Delims(left, right string) *Template
func (t *Template) Execute(wr io.Writer, data any) error
func (t *Template) ExecuteTemplate(wr io.Writer, name string, data any) error
func (t *Template) Funcs(funcMap FuncMap) *Template
func (t *Template) Lookup(name string) *Template
func (t *Template) Name() string
func (t *Template) New(name string) *Template
func (t *Template) Option(opt ...string) *Template
func (t *Template) Parse(text string) (*Template, error)
func (t *Template) ParseFS(fs fs.FS, patterns ...string) (*Template, error)
func (t *Template) ParseFiles(filenames ...string) (*Template, error)
func (t *Template) ParseGlob(pattern string) (*Template, error)
func (t *Template) Templates() []*Template
  • 对于函数,它们返回一个 Template 实例(简称为 t )。

  • 对于使用 t 作为参数的 Must() 函数和上面的 Template 方法,它们返回的 *Template 其实是原始实例 t

例如创建模板对象使用如下的方式:

t := template.New("abc")
tt,err := t.Parse("xxx")

这里的 ttt 都指向同一个模板对象,t 称为模板的关联名称(即创建了一个模板关联到变量 t 上),但 t 不是模板的名称,因为 Template 中有一个未导出的 name 字段才是模板的名称。

通过 Name() 方法可以返回 name 字段的值,name 也可以作为参数,一个关联名称 t(模板对象)上可以 “包含” 多个 name(多个模板)通过 t 和各自的 name 可以调用到指定的模板。


模板的结构


Template 与 common 的结构

Template 结构的定义如下:

type Template struct {name string*parse.Tree*commonleftDelim  stringrightDelim string
}

name 是这个 Template 的名称,Tree 是解析树,common 是模板组,leftDelimrightDelim 是左右两边的分隔符默认为 {{}}

common 结构的定义如下:

type common struct {tmpl   map[string]*Template // Map from name to defined templates.option optionmuFuncs    sync.RWMutex // protects parseFuncs and execFuncsparseFuncs FuncMapexecFuncs  map[string]reflect.Value
}

该结构体的第一个字段 tmpl 是一个 Templatemap 结构(key 为 template 的 name ,value 为 Template) ,一个 common 结构中可以包含多个 Template ,而 Template 结构中又指向了一个 common 结构。

common 是一个模板组,在这个模板组中的(tmpl 字段)所有 Template 都共享一个 common(模板组),模板组中包含 parseFuncsexecFuncs 字段(parseFuncsexecFuncs 这两个字段共同成为模板的 FuncMap )。


模板函数和方法


New() 函数和 init() 方法

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}
}

template.New() 函数不仅创建了一个模板,还创建了一个空的 common 结构,新创建的 common 是空的,只有在进行模板解析(Parse()ParseFiles() 方法等操作)之后才会将模板添加到 commontmpl 字段(map 结构)中,例如:

tmpl := template.New("mytmpl")

当调用 template.New() 函数后会生成 tmpl 为模板关联名称, mytmpl 为模板名称的模板对象。在 template 包中很多涉及到操作 Template 的函数、方法都会调用 init() 方法来保证返回的 Template 都有一个有效的 common 结构。在 init() 方法中会进行判断是否存在 common 的模板,对于已存在 common 的模板,不会新建 common 结构。

假设执行了 Parse() 方法,那么会把模板 name 添加到 common tmpl 字段的 map 结构中,其中模板 name 为 map 的 key ,模板为 map 的 value ,例如如下的程序:

package mainimport ("html/template""fmt"
)func main() {t1 := template.New("test1")tmpl,_ := t1.Parse(`{{define "T1"}}ONE{{end}}{{define "T2"}}TWO{{end}}{{define "T3"}}{{template "T1"}} {{template "T2"}}{{end}}{{template "T3"}}`)fmt.Println(t1)fmt.Println(tmpl)fmt.Println(t1.Lookup("test1"))      // 使用关联名称t1检索 test1 模板fmt.Println(t1.Lookup("T1"))fmt.Println(tmpl.Lookup("T2"))        // 使用关联名称tmpl检索 T2 模板fmt.Println(tmpl.Lookup("T3"))
}

执行程序输出如下的结果:

&{<nil> 0xc00007c080 0xc000104120 0xc00005c180}
&{<nil> 0xc00007c080 0xc000104120 0xc00005c180}
&{<nil> 0xc00007c080 0xc000104120 0xc00005c180}
&{<nil> 0xc00007c200 0xc000104240 0xc00005c180}
&{<nil> 0xc00007c240 0xc000104360 0xc00005c180}
&{<nil> 0xc00007c280 0xc000104480 0xc00005c180}

从程序运行的结果可知,前 3 行的结果完全一致,所有行的第二个地址完全相同,假设结果中的每一行都分为 3 列,第一列为 template name ,第二个字段为 parseTree 的地址,第三列为 common 结构的地址。因为 tmpl1、t1 都指向 test1 模板,所以前 3 行结果完全一致;因为 test1、T1、T2、T3 共享同一个 common ,所以第三列全都相同;因为每个模板的解析树不一样,所以第二列全都不一样。

该程序执行过程说明:

  • 首先使用 template.New() 函数创建了一个名为 test1 的模板,同时创建了一个模板组(common),它们关联在 t1 变量上。

  • 然后调用 Parse() 方法,在 Parse() 的待解析字符串中使用 define 又定义了 3 个新的模板对象,模板的 name 分别为 T1、T2 和 T3 ,其中 T1 和 T2 嵌套在 T3 中,因为调用的是 t1Parse() 方法,所以这 3 个新创建的模板都会关联到 t1 上。

  • 现在 t1 上关联了4个模板:test1、T1、T2、T3 ,它们全都共享同一个 common(模板组),因为已经执行了 Parse() 方法解析操作,这个 Parse() 操作会将 test1、T1、T2、T3 的 name 添加到 commontmplmap 结构中(即 commontmpl 字段的 map 结构中有 4 个元素)。

注意:虽然 test1、T1、T2、T3 都关联在 t1 上,但 t1 只能代表 test1 (所以上图中只有 test1 下面标注了t1),因为 t1 是一个 Template 类型。可以认为 test1、T1、T2、T3 这 4 个模板共享一个组,但 T1、T2、T3 都是对外部不可见的,只能通过特殊方法的查询找到它们。

template 包中很多返回*Template 的函数、方法返回的其实是原始的 t ,这个规则也适用于这里的 Parse() 方法,所以 tmplt1 这两个变量是完全等价的都指向同一个 Template(test1),所以前面的执行结果中前 3 行完全一致。


New() 方法

除了 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.init() 方法保证了有一个有效的 common 结构。

  • 然后构造一个新的 Template 对象 (nt) ,这个 nt 除了 name 和解析树 parse.Tree 字段之外,其它所有内容都和 t 完全一致,即 ntt 共享了同一个 common(模板组)。

New() 方法使得名为 name 的 nt 模板对象加入到了关联组中(即通过调用 t.New() 方法可以创建一个新的名为 name 的模板对象,并将此对象加入到 t 模板组中),这和 New() 函数的作用基本是一致的,只不过 New() 函数是构建新的模板对象并构建一个新的 common 结构,而 New() 方法则是构建一个新的模板对象,并加入到已有的 common 结构中。

因为 New() 出来的新对象在执行解析之前(如 Parse() 方法),它们暂时都还不会加入到 common 组中,在 New() 操作之后,仅仅只是让它指向已有的一个 common 结构。

例如编写如下程序:

t1 := template.New("test1")
t1 = t1.Parse(...)
t2 := t1.New("test2")
t2 = t2.Parse(...)
t3 := t1.New("test3")

如果 t1t2Parse() 方法中都定义一个或多个 name 相同的模板,例如编写如下程序:

package mainimport ("html/template""os"
)
func main(){t1 := template.New("test1")t2 := t1.New("test2")t1, _ = t1.Parse(`{{define "T1"}}Hi{{end}}{{define "T2"}}cqupthao{{end}}{{define "T3"}}{{template "T1"}} {{template "T2"}}{{end}}{{template "T3"}}`)t2, _ = t2.Parse(`{{define "T4"}}Hello{{end}}{{define "T2"}}CQUPT!{{end}}{{define "T3"}}{{template "T4"}} {{template "T2"}}{{end}}{{template "T3"}}`)_ = t1.Execute(os.Stdout, "a")_ = t2.Execute(os.Stdout, "a")
}

执行程序输出如下结果:

 Hello CQUPT!Hello CQUPT!

该程序的 t1t2 中共享了同一个 commont1.Parse() 方法中定义了 T1、T2 和 T3 ,t2.Parse() 方法中定义了 T4、T2 和 T3 且两个 T2 的解析内容不一样(解析树不一样),由于 T1、T2、T3、T4 都会加入到 t1t2 共享的 common 中,所以无论是通过 t1 还是通过 t2 这两个关联名称都能找到 T1、T2、T3、T4 。

由于后解析的会覆盖先解析的,所以无论调用 t1.Lookup("T2") 还是 t2.Lookup("T2") 得到的 T2 对应的 template 都是在 t2.Parse() 方法中定义的,当调用 t1.Execute() 方法会得到 t2 中定义的 T2 的值。


Parse() 方法

Parse(string) 方法用于解析给定的文本内容 string ,Parse() 方法是解析字符串的且只解析 New() 出来的模板对象。

  • 当创建了一个模板对象后,会有一个与之关联的 common (如果不存在,template 包中的各种函数、方法都会因为调用 init() 方法而保证 common 的存在)。

  • 只有在 Parse() 之后才会将相关的 template name 放进 common 中,表示这个模板已经可用了或者称为已经定义了(defined),可用被Execute()ExecuteTemplate()函数渲染,也表示可用使用 Lookup()DefinedTemplates() 来检索模板。

  • 调用了 Parse() 方法解析后会将给定的 FuncMap 中的函数添加到 commonFuncMap 中,只有添加到 common 的函数,才可以在模板中使用。


ParseFiles() 方法/函数和 ParseGlob() 方法/函数

Parse() 方法只能解析字符串,若要解析文件中的内容需要使用 ParseFiles() 函数和 ParseGlob() 函数 或 ParseFiles() 方法和 ParseGlob() 方法,其函数 / 方法的声明如下:

func ParseFiles(filenames ...string) (*Template, error)
func ParseGlob(pattern string) (*Template, error)
func (t *Template) ParseFiles(filenames ...string) (*Template, error)
func (t *Template) ParseGlob(pattern string) (*Template, error)

这两个函数和这两个方法的区别,ParseFiles() 函数是直接解析一个或多个文件的内容并返回第一个文件名的 basename 作为 Template 的名称(即这些文件的 Template 全都关联到第一个文件的 basename 上),ParseFiles() 方法则是解析一个或多个文件的内容,并将这些内容关联到 t 上。

例如当前 Go 程序项目的目录下有任意内容的 3 个文件(a.cnfb.cnfc.cnf ),编写如下程序:

package mainimport ("html/template""fmt"
)func main() {t1,err := template.ParseFiles("a.cnf","b.cnf","c.cnf")if err != nil {panic(err)}// t1.Parse("")fmt.Println(t1.DefinedTemplates())fmt.Println()fmt.Println(t1)fmt.Println(t1.Lookup("a.cnf"))fmt.Println(t1.Lookup("b.cnf"))fmt.Println(t1.Lookup("c.cnf"))
}

执行程序输出的结果如下:

; defined templates are: "a.cnf", "b.cnf", "c.cnf"&{<nil> 0xc00007c080 0xc000104120 0xc00005c1e0}
&{<nil> 0xc00007c080 0xc000104120 0xc00005c1e0}
&{<nil> 0xc00007c0c0 0xc000104240 0xc00005c1e0}
&{<nil> 0xc00007c100 0xc000104360 0xc00005c1e0}

从程序运行的结果可知,已定义的 template name 都是文件的 basenamet1a.cnf 这个 template 是完全一致的(即 t1 是文件列表中的第一个模板对象)。因为 template.New() 函数创建的模板对象 test 并没有包含到 common 中,因为 t.ParseFiles() 方法和 t.ParseGlob() 方法的解析过程是独立于 t 之外的,它们只解析文件内容,不解析字符串,而 New() 出来的模板需要 Parse() 方法来解析才会加入到 common 中。

将上面的注释行取消掉,执行程序的结果将如下:

; defined templates are: "b.cnf", "c.cnf", "a.cnf"&{<nil> 0xc00007c080 0xc000104120 0xc00005c1e0}
&{<nil> 0xc00007c080 0xc000104120 0xc00005c1e0}
&{<nil> 0xc00007c0c0 0xc000104240 0xc00005c1e0}
&{<nil> 0xc000182000 0xc00018a000 0xc00005c1e0}

具体原因参考 parseFiles() 函数的源码部分,具体的程序代码如下:

func parseFiles(t *Template, filenames ...string) (*Template, error) {if len(filenames) == 0 {// Not really a problem, but be consistent.return nil, fmt.Errorf("template: no files named in call to ParseFiles")}for _, filename := range filenames {b, err := ioutil.ReadFile(filename)if err != nil {return nil, err}s := string(b)// name为文件名的basename部分name := filepath.Base(filename)var tmpl *Templateif t == nil {t = New(name)}// 如果调用t.Parsefiles(),则t.Name不为空// name也就不等于t.Name// 于是新New(name)一个模板对象给tmplif name == t.Name() {tmpl = t} else {tmpl = t.New(name)}// 解析tmpl。如果选中了上面的else分支,则和t无关_, err = tmpl.Parse(s)if err != nil {return nil, err}}return t, nil
}

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 中某个已定义的模板进行应用,例如编写如下程序:

package mainimport ("html/template""fmt""os"
)func main() {t1 := template.New("test1")t1, _ = t1.Parse(`{{define "T1"}}ONE{{end}}{{- define "T2"}}TWO{{end}}{{- define "T3"}}{{template "T1"}} {{template "T2"}}{{end}}{- template "T3"}}`)_ = t1.Execute(os.Stdout,"")fmt.Println()fmt.Println("-------------")_ = t1.ExecuteTemplate(os.Stdout, "T2", "")
}

执行程序输出的结果如下:

ONE TWO
-------------
TWO

Lookup() 方法、DefinedTemplates() 方法和 Templates() 方法

Lookup() 方法、DefinedTemplates() 方法和 Templates() 方法都用于检索已经定义的模板。

  • Lookup() 方法会根据 template name 来检索并返回对应的 template
  • DefinedTemplates() 方法则是返回所有已定义的 templates
  • Templates() 方法和 DefinedTemplates() 方法类似,但是它返回的是 []*Template ,也就是已定义的 templateslice

只有在解析之后模板才加入到 common 结构中,才能算是已经定义,才能被检索或执行,当检索不存在的 templates 时,Lookup() 方法将返回 nil ,当 common 中没有模板,DefinedTemplates() 方法将返回空字符串,Templates() 方法将返回空的 slice

例如编写如下程序:

package mainimport ("html/template""fmt"
)
func main() {t1 := template.New("test1")t2 := t1.New("test2")t1, _ = t1.Parse(`{{define "T1"}}ONE{{end}}{{define "T2"}}TWO{{end}}{{define "T3"}}{{template "T1"}} {{template "T2"}}{{end}}{{template "T3"}}`)t2, _ = t2.Parse(`{{define "T4"}}ONE{{end}}{{define "T2"}}TWOO{{end}}{{define "T3"}}{{template "T4"}} {{template "T2"}}{{end}}{{template "T3"}}`)fmt.Println(t1.DefinedTemplates())fmt.Println(t2.DefinedTemplates())fmt.Println(t2.Templates())
}

执行程序输出如下的结果:

; defined templates are: "T1", "T2", "T3", "test1", "T4", "test2"
; defined templates are: "test2", "T1", "T2", "T3", "test1", "T4"
[0xc0000a05a0 0xc0000a05d0 0xc0000a0840 0xc0000a0870 0xc0000a08a0 0xc0000a0b40]

从程序运行的结果可知,虽然返回的顺序虽然不一致,但包含的 template name 是完全一致的。


Clone() 方法

Clone() 方法用于克隆一个完全一样的模板包括 common 结构也会完全克隆,例如如下的程序部分:

t1 := template.New("test1")
t1 = t1.Parse(...)t2 := t1.New("test2")
t2 = t2.Parse(...)t3, err := t1.Clone()
if err != nil {panic(err)
}

这里的 t3t1 在内容上完全一致,但在内存中它们是两个不同的对象, t3 中会包含 t1t2 共享的 common ,即使 t2 中定义了 {{define "Tx"}}...{{end}} ,这个 Tx 也会包含在 t3 中,因为是不同的对象,所以修改 t3 不会影响 t1/t2 ,例如编写如下程序:

package mainimport ("html/template""fmt"
)func main() {t1 := template.New("test1")t2 := t1.New("test2")t1, _ = t1.Parse(`{{define "T1"}}ONE{{end}}{{define "T2"}}TWO{{end}}{{define "T3"}}{{template "T1"}} {{template "T2"}}{{end}}{{template "T3"}}`)t2, _ = t2.Parse(`{{define "T4"}}ONE{{end}}{{define "T2"}}TWOO{{end}}{{define "T3"}}{{template "T4"}} {{template "T2"}}{{end}}{{template "T3"}}`)t3, err := t1.Clone()if err != nil {panic(err)}// 结果完全一致fmt.Println(t1.Lookup("T4"))fmt.Println(t3.Lookup("T4"))// 修改 t3t3,_ = t3.Parse(`{{define "T4"}}one{{end}}`)// 结果将不一致fmt.Println(t1.Lookup("T4"))fmt.Println(t3.Lookup("T4"))
}

执行程序输出的结果如下:

&{<nil> 0xc00007c500 0xc0001046c0 0xc00005c180}
&{<nil> 0xc00007c700 0xc000104a20 0xc00005c2a0}
&{<nil> 0xc00007c500 0xc0001046c0 0xc00005c180}
&{<nil> 0xc00007c940 0xc000105200 0xc00005c2a0}

从程序运行的结果可知, t3t1 在内容上完全一致,在内存中它们是两个不同的对象, t3 中会包含 t1t2 共享的 common ,这个 Tx 也会包含在 t3 中,修改 t3 不会影响 t1/t2


Must() 函数

正常情况下很多函数、方法都返回两个值(一个是需要返回的值,一个是 err 信息,template 包中的函数、方法也一样如此,但有时候不需要返回 err 信息,而是直接取第一个返回值并赋值给变量,操作过程例如如下程序部分:

t1 := template.New("ttt")
t1,err := t1.Parse(...)
if err != nil {panic(err)
}

Must() 函数将上面的过程进行了封装,简化后的程序代码如下:

func Must(t *Template, err error) *Template {if err != nil {panic(err)}return t
}

当某个返回 *Template,err 的函数、方法需要直接使用时,可用将其包装在 Must() 函数中,它会自动在有 err 的时候 panic ,无错的时候只返回其中的 *Template 对象。这在赋值给变量的时候非常简便,例如如下程序部分:

var t = template.Must(template.New("name").Parse("text"))

FuncMap 与 Funcs() 函数

template 允许定义自己的函数添加到 common 中,然后就可以在待解析的内容中像使用内置函数一样使用自定义的函数。自定义函数的优先级高于内置的函数优先级(即先检索自定义函数,再检索内置函数,如果自定义函数的函数名和内置函数名相同,则内置函数将失效)。

common 结构中有一个字段是 FuncMap 类型, common 结构的定义如下:

type common struct {tmpl   map[string]*Templateoption optionmuFuncs    sync.RWMutex // protects parseFuncs and execFuncsparseFuncs FuncMapexecFuncs  map[string]reflect.Value
}

FuncMap 结构的定义如下:

type FuncMap map[string]interface{}

它是一个 map 结构,key 为模板中可以使用的函数名,value 为函数对象(函数),函数必须只有 1 个值或 2 个值,如果有两个值,第二个值必须是 error 类型的,当执行函数时 err 不为空,则执行自动停止。

函数可以有多个参数,假如函数 str 有两个参数,在待解析的内容中调用函数 str 时,如果调用方式为 {{str . "aaa"}} ,表示第一个参数为当前对象,第二个参数为字符串 “aaa” 。

假如定义一个将字符串转换为大写的函数,编写程序如下:

import "strings"
func upper(str string) string {return strings.ToUpper(str)
}

将其添加到 FuncMap 结构中并将此函数命名为 “strupper” ,以后在待解析的内容中就可以调用 “strupper” 函数,编写程序如下:

funcMap := template.FuncMap{"strupper": upper,
}

或者直接将匿名函数放在 FuncMap 内部:

funcMap := template.FuncMap{"strupper": func(str string) string { return strings.ToUpper(str) },
}

上面程序只是定义了一个 FuncMap 实例,这个实例中有一个函数还没有将它关联到模板,严格地说还没有将其放进 common 结构,若要将其放进 common 结构,需要调用 Funcs() 方法(其实调用此方法也没有将其放进 common ,只有在解析的时候才会放进 common ), Funcs() 方法的声明如下:

func (t *Template) Funcs(funcMap FuncMap) *Template

具体的实现,参考如下程序代码:

funcMap := template.FuncMap{"strupper": func(str string) string { return strings.ToUpper(str) },
}
t1 := template.New("test")
t1 = t1.Funcs(funcMap)

这样和 t1 共享 common 的所有模板都可以调用 “strupper” 函数(注:必须在解析之前调用 Funcs() 方法,在解析的时候会将函数放进 common 结构)。

完整的程序代码如下:

package mainimport ("os""strings""text/template"
)func main() {funcMap := template.FuncMap{"strupper": upper,}t1 := template.New("test1")tmpl, err := t1.Funcs(funcMap).Parse(`{{strupper .}}`)if err != nil {panic(err)}_ = tmpl.Execute(os.Stdout, "Welcome to CQUPT!\n")
}func upper(str string) string {return strings.ToUpper(str)
}

执行程序输出的结果如下:

WELCOME TO CQUPT!

从程序运行的结果可知,上面调用了 {{strupper .}} ,这里的 strupper 是自定义的函数,“.” 是它的参数(注意:参数不是放进括号里)这里的 “.” 代表当前作用域内的当前对象,对于这个示例来说,当前对象就是那段字符串对象 “Welcome to CQUPT!”。


模板处理过程


模板处理流程(构建模板对象 New() --> 解析数据Parse() --> 应用合并 Execute() )主要是如下两个阶段 :

  • 第一个阶段是 parse 阶段,在这个阶段会把读入的数据,不论是从文件读入还是直接从字符串读取的内容统一解析成节点树。

  • 第二个阶段是 execute 阶段,在这个阶段会把传进来的变量解析到节点树上,生成最终的输出流,然后写入到 io.Writer 中。

#mermaid-svg-X1U27ZJrVvI3Bm0N {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-X1U27ZJrVvI3Bm0N .error-icon{fill:#552222;}#mermaid-svg-X1U27ZJrVvI3Bm0N .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-X1U27ZJrVvI3Bm0N .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-X1U27ZJrVvI3Bm0N .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-X1U27ZJrVvI3Bm0N .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-X1U27ZJrVvI3Bm0N .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-X1U27ZJrVvI3Bm0N .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-X1U27ZJrVvI3Bm0N .marker{fill:#333333;stroke:#333333;}#mermaid-svg-X1U27ZJrVvI3Bm0N .marker.cross{stroke:#333333;}#mermaid-svg-X1U27ZJrVvI3Bm0N svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-X1U27ZJrVvI3Bm0N .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-X1U27ZJrVvI3Bm0N .cluster-label text{fill:#333;}#mermaid-svg-X1U27ZJrVvI3Bm0N .cluster-label span{color:#333;}#mermaid-svg-X1U27ZJrVvI3Bm0N .label text,#mermaid-svg-X1U27ZJrVvI3Bm0N span{fill:#333;color:#333;}#mermaid-svg-X1U27ZJrVvI3Bm0N .node rect,#mermaid-svg-X1U27ZJrVvI3Bm0N .node circle,#mermaid-svg-X1U27ZJrVvI3Bm0N .node ellipse,#mermaid-svg-X1U27ZJrVvI3Bm0N .node polygon,#mermaid-svg-X1U27ZJrVvI3Bm0N .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-X1U27ZJrVvI3Bm0N .node .label{text-align:center;}#mermaid-svg-X1U27ZJrVvI3Bm0N .node.clickable{cursor:pointer;}#mermaid-svg-X1U27ZJrVvI3Bm0N .arrowheadPath{fill:#333333;}#mermaid-svg-X1U27ZJrVvI3Bm0N .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-X1U27ZJrVvI3Bm0N .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-X1U27ZJrVvI3Bm0N .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-X1U27ZJrVvI3Bm0N .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-X1U27ZJrVvI3Bm0N .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-X1U27ZJrVvI3Bm0N .cluster text{fill:#333;}#mermaid-svg-X1U27ZJrVvI3Bm0N .cluster span{color:#333;}#mermaid-svg-X1U27ZJrVvI3Bm0N div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-X1U27ZJrVvI3Bm0N :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}

template.Template
Parse阶段
Execute阶段

例如编写如下程序:

package mainimport ("os""text/template"
)
var templateStr = `Welcome to CQUPT!`func main() {// 创建 template 对象t := template.New("demo")// 解析模板内容parse, err := t.Parse(templateStr)if err != nil {panic(err)}// 执行解析data := map[string]string{"Name": "World"}err = parse.Execute(os.Stdout, data)if err != nil {panic(err)}
}

执行程序输出的结果如下:

Welcome to CQUPT!

程序执行过程分析:

  • template.New() 创建了一个 template 对象。
type Template struct {name string*parse.Tree*commonleftDelim  stringrightDelim string
}
  • Parse 阶段。

*parse.Tree 只在 html/template 中使用,表示解析完成的模板,common 里包含了解析过程中所有输入的模板,用一个 map 存储,因为 template 支持 {{define block}} 这种嵌套模板,每一个模板快都会被解析成一个 Template 对象,然后放到这个 map 里供模板之间引用,每个模板解析完之后会生成 parse.Tree 结构。

上面的程序生成的模板对象 t 包含的 map 只有一个元素,元素的 Root 属性(parse 之后生成的 parse.Tree)会有三个节点,其中 TextNode 直接存储了文本,ActionNode 会解析出 .Name 变量将文本字符串解析成为 parse.Tree 对象之后,Parse 阶段就执行结束了。

在这个阶段输入内容经历了从 template.Templateparse.NodeList 再到 parse.Node几个步骤,将纯文本变成了可以统一处理的节点。

  • Execute 阶段。

template 包提供的 Execute() 方法关键部分源码如下:


func (t *Template) Execute(wr io.Writer, data any) error {return t.execute(wr, data)
}func (t *Template) execute(wr io.Writer, data any) (err error) {defer errRecover(&err)// 使用反射解析传入的参数value, ok := data.(reflect.Value)...state := &state{tmpl: t,wr:   wr,vars: []variable{{"$", value}},}...// 遍历节点state.walk(value, t.Root)return
}

Execute 阶段开始的时会先反射传入的 data 用来解析模板内的变量,接下来遍历模板的Root 也就是从节点树的根开始使用递归的方式遍历处理每个节点,关键程序代码部分如下:

func (s *state) walk(dot reflect.Value, node parse.Node) {s.at(node)switch node := node.(type) {case *parse.ActionNode:val := s.evalPipeline(dot, node.Pipe)if len(node.Pipe.Decl) == 0 {s.printValue(node, val)}...case *parse.ListNode:// 循环遍历节点, 递归 for _, node := range node.Nodes {s.walk(dot, node)}...case *parse.TextNode:if _, err := s.wr.Write(node.Text); err != nil {s.writeError(err)}...}...
}

其中 ActionNode() 方法和 TextNode() 方法的处理如下:

ActionNode 在 Parse 阶段生成语法树,在 Execute 阶段分为两步处理:

第一步是解析语法树,把对应的变量替换成实际的值,对应方法 evalPipeline() 部分。

第二步是把生成的结果输出,对应了代码 printValue() 部分。

TextNode 直接把 Node 存储的文本原封不动的打印,直接使用了 Write() 方法写入,节点遍历完成之后,所有的文本已经都输出到 io.Writer 中,模板执行结束。

在这个阶段遍历了全部的 parse.Node,根据不同的规则把每个 node 的内容处理过之后输出到 io.Writer 里完成执行。


  • 参考链接 深入解析Go template模板使用详解

  • 参考链接 Golang template 包基本原理分析

GO 语言中模板渲染的原理相关推荐

  1. c语言va_start函数,va_start和va_end,以及c语言中的可变参数原理

    FROM:http://www.cnblogs.com/hanyonglu/archive/2011/05/07/2039916.html 本文主要介绍va_start和va_end的使用及原理. 在 ...

  2. c语言中姓名查找的原理,搜索 C/C++ 函数调用原理

    在 C/C++ 函数调用的整个过程中内存空间进行了什么操作?本文对 C/C++ 函数调用原理进行扼要说明. 一.预备知识 (一) 内存中数据的地址 地址在内存中存放时可能会跨越连续若干个存储单元(一个 ...

  3. c语言冒泡排序的两种实现方式,c语言中冒泡排序的实现原理是什么?

    满意答案 jmytwdipp 推荐于 2017.10.08 采纳率:54%    等级:11 已帮助:9051人 冒泡排序,就是对一组数进行逐趟排序的方法,具体分为升序和降序. 以升序为例. 每一趟的 ...

  4. c语言中函数调用的原理

     一. 函数参数传递机制的基本理论  函数参数传递机制问题在本质上是调用函数(过程)和被调用函数(过程)在调用发生时进行通信的方法问题.基本的参数传递机制有两种:值传递和引用传递.以下讨论称调用其 ...

  5. Django学习笔记之模板渲染、模板语言、simple_tag、母版子版、静态配置文件

    一.首先我们用PyCharm来创建一个Django项目 终端命令:django-admin startproject sitename 图形创建: 这样一个Django项目就创建完成了,上面可以看到项 ...

  6. spring mvc 渲染html,在Spring MVC中使用Thymeleaf模板渲染Web视图

    Thymeleaf模板是原生的,不依赖于标签库.它能在接受原始HTML的地方进行编辑和渲染.由于没有与Servlet规范耦合,因此Thymeleaf模板能够进入JSP所无法涉及的领域 如果想要在Spr ...

  7. C语言中比较大小的函数模板,C语言中实现模板函数小结 : 不敢流泪

    --by boluor 2009/5/20 如果要写个函数支持多种数据类型,首先想到的就是C++的模板了,但是有时候只能用C语言,比如在linux内核开发中,为了减少代码量,或者是某面试官的要求- 考 ...

  8. c语言函数编写格式,在c语言中如何实现函数模板?

    如果要写个函数支持多种数据类型,首先想到的就是C++的模板了,但是有时候只能用C语言,比如在linux内核开发中,为了减少代码量,或者是某面试官的要求- 考虑了一阵子后,就想到了qsort上.qsor ...

  9. c语言block内部的实现原理,iOS中block变量捕获原理详析

    Block概述 Block它是C语言级别和运行时方面的一个特征.Block封装了一段代码逻辑,也用{}括起,和标准C语言中的函数/函数指针很相似,此外就是blokc能够对定义环境中的变量可以引用到.这 ...

最新文章

  1. 云计算推进企业管理深化,私有云将会深入企业
  2. pyqt5 输入确认_对PyQt5的输入对话框使用(QInputDialog)详解
  3. all any 或 此运算符后面必须跟_PHP程序员必须会的 45 个PHP 面试题(第一部分)...
  4. Windows下安装MySQL压缩zip包
  5. java开关用法_如何在Java中使用带开关盒的枚举?
  6. :after伪类+content内容清除浮动
  7. Hive thrift服务--beeline使用
  8. jvm性能调优工具之 jmap使用详解
  9. SSL introduce itself from baidu
  10. 计算机表演赛怎么打用户名,赛事报名操作指南 | 第28届中国儿童青少年威盛中国芯HTC计算机表演赛...
  11. python实现阿拉伯数字和罗马数字的互相转换
  12. GCC和C99标准中inline
  13. 学计算机做近视眼手术,做完近视眼手术后多久可以看电脑
  14. 微生物组实验手册:中科院、北大和清华等52家单位的74个团队的153篇方法正在创作中(15篇已投稿)...
  15. 实时股价——可以查询股票当前价格。用户可以设定数据刷新频率,程序会用绿色和红色的箭头表示股价走势。
  16. 综合各代码在线运行 jsrun
  17. java 图形 登录_java登录图形界面 - osc_994n5tsc的个人空间 - OSCHINA - 中文开源技术交流社区...
  18. eclipse网络代理设置
  19. ts类型声明declare
  20. 技术管理进阶——总监以上一定要会经济学

热门文章

  1. 基于android的美食食谱分享推荐系统app
  2. 计算机组成原理总复习——题目练习
  3. .mht文件转换为html
  4. 服务器性能主要指标,性能测试中服务器关键性能指标浅析
  5. 【SSL】2128可可摘苹果
  6. 毛球科技点评区块链在组织和商业生态系统中的未来
  7. TCP协议为什么需要三次握手?
  8. MySQL + navicat
  9. python中time库的时间单位是秒
  10. myeclipse添加oracle,向MyEclipse添加Oracle数据库