Go标准库:深入剖析Go template(上)

简介: Go标准库:深入剖析Go template

本文只关注Go text/template的底层结构,带上了很详细的图片以及示例帮助理解,有些地方也附带上了源码进行解释。有了本文的解释,对于Go template的语法以及html/template的用法,一切都很简单。


关于template的语法以及具体使用方法,见:Go template用法详解


入门示例

package main
import (
    "html/template""os"
)
type Person struct {
    Name string
    Age    int
}
funcmain() {
    p := Person{"longshuai", 23}
    tmpl, err := template.New("test").Parse("Name: {{.Name}}, Age: {{.Age}}")
    if err != nil {
        panic(err)
    }
    err = tmpl.Execute(os.Stdout, p)
    if err != nil {
        panic(err)
    }
    fmt.Println(tmpl)
}

上面定义了一个Person结构,有两个****大写字母开头(意味着这俩字段是导出的)的字段Name和Age。然后main()中创建了Person的实例对象p。

紧接着使用template.New()函数创建了一个空Template实例(对象),然后通过这个template实例调用Parse()方法,Parse()方法用来解析、评估模板中需要执行的action,其中需要评估的部分都使用 {{}}包围,并将评估后(解析后)的结果赋值给tmpl。

最后调用Execute()方法,该方法将数据对象Person的实例p应用到已经解析的tmpl模板,最后将整个应用合并后的结果输出到os.Stdout。

上面的示例很简单,两个注意点:


  1. 流程:构建模板对象New()-->解析数据Parse()-->应用合并Execute()
  2. Parse()解析的对象中包含了{{}},其中使用了点(.),{{.Name}}代表Execute()第二个参数p对象的Name字段,同理 {{.Age}}


也就是说,{{.}}代表的是要应用的对象,类似于java/c++中的this,python/perl中的self。


更通用地,{{.}}表示的是所处作用域的当前对象,而不仅仅只代表Execute()中的第二个参数对象。例如,本示例中 {{.}}代表顶级作用域的对象p,如果Parse()中还有嵌套的作用域range,则 {{.}}代表range迭代到的每个元素对象。如果了解perl语言,{{.}}可以理解为默认变量 $_。


模板关联(associate)

template中有不少函数、方法都直接返回*Template类型。



6ea13742774b2230a022e8a8183a5d3d.png

上图中使用红色框线框起来一部分返回值是*Template的函数、方法。对于函数,它们返回一个Template实例(假设为t),对于使用t作为参数的Must()函数和那些框起来的Template方法,它们返回的 *Template其实是原始实例t

例如:

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


这里的t和tt其实都指向同一个模板对象。

这里的****t称为模板的关联名称。通俗一点,就是创建了一个模板,关联到变量t上。但注意,t不是模板的名称,因为Template中有一个未导出的name字段,它才是模板的名称。可以通过Name()方法返回name字段的值,而且仔细观察上面的函数、方法,有些是以name作为参数的。

之所以要区分模板的关联名称(t)和模板的名称(name),是因为****一个关联名称t(即模板对象)上可以"包含"多个name,也就是多个模板,通过t和各自的name,可以调用到指定的模板。

模板结构详解


首先看Template结构:


type Template struct {
    name string*parse.Tree
    *common
    leftDelim  string
    rightDelim string
}


name是这个Template的名称,Tree是解析树,common是另一个结构,稍后解释。leftDelim和rightDelim是左右两边的分隔符,默认为{{}}

这里主要关注name和common两个字段,name字段没什么解释的。common是一个结构:

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。

大概结构如下图:

fc1398c23332e0bef6e249a68078828d.png


除了需要关注的name和common,parseFuncs和execFuncs这两个字段也需要了解下,它们共同成为模板的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()等操作)之后,才会将模板添加到common的tmpl字段(map结构)中。

所以,下面的代码:


tmpl := template.New("mytmpl1")


执行完后将生成如下结构,其中tmpl为模板关联名称,mytmpl1为模板名称。

9e9f97eb74555db9d5185b760b51c1db.png


因为还没有进行解析操作,所以上图使用虚线表示尚不存在的部分。

实际上,在template包中,很多涉及到操作Template的函数、方法,都会调用init()方法保证返回的Template都有一个有效的common结构。当然,因为init()方法中进行了判断,对于已存在common的模板,不会新建common结构。

假设现在执行了Parse()方法,将会把模板name添加到common tmpl字段的map结构中,其中模板name为map的key,模板为map的value。

例如:

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

上述代码的执行结果:注意前3行的结果完全一致,所有行的第二个地址完全相同。

&{test1 0xc0420a60000xc0420640c0  }
&{test1 0xc0420a60000xc0420640c0  }
&{test1 0xc0420a60000xc0420640c0  }
&{T1 0xc0420a61000xc0420640c0  }
&{T2 0xc0420a62000xc0420640c0  }
&{T3 0xc0420a63000xc0420640c0  }



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

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

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

结构如下图:

e27215b2207ad25c16c11be8abd0c01f.png


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

另外,前文说过,template包中很多返回 *Template的函数、方法返回的其实是原始的t(看源代码即可知道),这个规则也适用于这里的Parse()方法,所以tmpl和t1这两个变量是完全等价的,都指向同一个template,即test1。所以前面的执行结果中前3行完全一致。

**再回头看上面代码的执行结果,假设结果中的每一行都分为3列,**第一列为template name,第二个字段为parseTree的地址,第三列为common结构的地址。因为tmpl1、t1都指向test1模板,所以前3行结果完全一致。因为test1、T1、T2、T3共享同一个common,所以第三列全都相同。因为每个模板的解析树不一样,所以第二列全都不一样。


New()方法

除了template.New()函数,还有一个Template.New()方法:

// New allocates a new, undefined template associated with the given one and with the same// delimiters. The association, which is transitive, allows one template to// invoke another with a {{template}} action.
func (t *Template) New(name string) *Template {
    t.init()
    nt :=&Template{
        name:       name,
        common:     t.common,
        leftDelim:  t.leftDelim,
        rightDelim: t.rightDelim,
    }
    return nt
}


看注释很难理解,但是看它的代码,结合前文的解释,New()方法的作用很明显。

首先t.init()保证有一个有效的common结构,然后构造一个新的Template对象nt,这个nt除了name和解析树parse.Tree字段之外,其它所有内容都和t完全一致。换句话说,nt和t共享了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")

结构图:


2cd102742515096a58d4420bb19aebea.png


相关文章
|
6天前
|
安全 Go
Golang深入浅出之-Go语言模板(text/template):动态生成HTML
【4月更文挑战第24天】Go语言标准库中的`text/template`包用于动态生成HTML和文本,但不熟悉其用法可能导致错误。本文探讨了三个常见问题:1) 忽视模板执行错误,应确保正确处理错误;2) 忽视模板安全,应使用`html/template`包防止XSS攻击;3) 模板结构不合理,应合理组织模板以提高可维护性。理解并运用这些最佳实践,能提升Go语言模板编程的效率和安全性,助力构建稳健的Web应用。
28 0
|
6天前
|
数据采集 存储 Go
使用Go语言和chromedp库下载Instagram图片:简易指南
Go语言爬虫示例使用chromedp库下载Instagram图片,关键步骤包括设置代理IP、创建带代理的浏览器上下文及执行任务,如导航至用户页面、截图并存储图片。代码中新增`analyzeAndStoreImage`函数对图片进行分析和分类后存储。注意Instagram的反爬策略可能需要代码适时调整。
使用Go语言和chromedp库下载Instagram图片:简易指南
|
6天前
|
SQL 开发框架 .NET
你确定不学?Go标准库之 text/template
你确定不学?Go标准库之 text/template
11 2
|
6天前
|
运维 监控 Go
Golang深入浅出之-Go语言中的日志记录:log与logrus库
【4月更文挑战第27天】本文比较了Go语言中标准库`log`与第三方库`logrus`的日志功能。`log`简单但不支持日志级别配置和多样化格式,而`logrus`提供更丰富的功能,如日志级别控制、自定义格式和钩子。文章指出了使用`logrus`时可能遇到的问题,如全局logger滥用、日志级别设置不当和过度依赖字段,并给出了避免错误的建议,强调理解日志级别、合理利用结构化日志、模块化日志管理和定期审查日志配置的重要性。通过这些实践,开发者能提高应用监控和故障排查能力。
89 1
|
6天前
|
安全 Go
Golang深入浅出之-Go语言标准库中的文件读写:io/ioutil包
【4月更文挑战第27天】Go语言的`io/ioutil`包提供简单文件读写,适合小文件操作。本文聚焦`ReadFile`和`WriteFile`函数,讨论错误处理、文件权限、大文件处理和编码问题。避免错误的关键在于检查错误、设置合适权限、采用流式读写及处理编码。遵循这些最佳实践能提升代码稳定性。
24 0
|
6天前
|
NoSQL Shell Go
在go中简单使用go-redis库
在go中简单使用go-redis库
|
6天前
|
安全 Go 开发者
Golang深入浅出之-Go语言模板(text/template):动态生成HTML
【4月更文挑战第25天】Go语言的`text/template`和`html/template`库提供动态HTML生成。本文介绍了模板基础,如基本语法和数据绑定,以及常见问题和易错点,如忘记转义、未初始化变量、复杂逻辑处理和错误处理。建议使用`html/template`防止XSS攻击,初始化数据结构,分离业务逻辑,并严谨处理错误。示例展示了条件判断和循环结构。通过遵循最佳实践,开发者能更安全、高效地生成HTML。
25 0
|
6天前
|
中间件 Go API
Golang深入浅出之-Go语言标准库net/http:构建Web服务器
【4月更文挑战第25天】Go语言的`net/http`包是构建高性能Web服务器的核心,提供创建服务器和发起请求的功能。本文讨论了使用中的常见问题和解决方案,包括:使用第三方路由库改进路由设计、引入中间件处理通用逻辑、设置合适的超时和连接管理以防止资源泄露。通过基础服务器和中间件的代码示例,展示了如何有效运用`net/http`包。掌握这些最佳实践,有助于开发出高效、易维护的Web服务。
29 1
|
6天前
|
Java 测试技术 Go
Go语言标准库进阶应用与最佳实践:提升代码质量与性能
【2月更文挑战第8天】在掌握了Go语言标准库的基础应用后,如何进一步发掘其潜力,提升代码质量和性能,是每位Go语言开发者所关心的问题。本文将探讨Go语言标准库的进阶应用与最佳实践,包括标准库与其他库的协同使用、性能优化与内存管理、错误处理与异常捕获、标准库在实际项目中的应用案例,以及推荐的最佳实践与编程规范。通过深入了解这些内容,开发者能够更好地利用Go语言标准库,提升代码质量与性能,构建出更加高效、可靠的软件应用。
|
23小时前
|
存储 安全 编译器
go语言中进行不安全的类型操作
【5月更文挑战第10天】Go语言中的`unsafe`包提供了一种不安全但强大的方式来处理类型转换和底层内存操作。包含两个文档用途的类型和八个函数,本文也比较了不同变量和结构体的大小与对齐系数,强调了字段顺序对内存分配的影响。
32 8
go语言中进行不安全的类型操作