在现代软件开发中,数据驱动的应用程序逐渐成为主流。无论是构建动态网站、代码生成,生成配置文件,还是创建复杂的文档模板,数据驱动的方式都能显著提升开发效率和代码可维护性。在 Go 语言中,text/template
包提供了一种强大的方式来处理文本和数据的结合。
一、基础功能回顾
文本和空格
在 text/template
中,文本和空格的处理直接影响到最终输出的格式。模板中的文本会原样输出,而空格和换行符会保留。
package main import ( "os" "text/template" ) func main() { const templateText = `Hello, {{.Name}}! Welcome to the Go template tutorial.` tmpl, err := template.New("example").Parse(templateText) if err != nil { panic(err) } data := struct { Name string }{ Name: "go-zero", } err = tmpl.Execute(os.Stdout, data) if err != nil { panic(err) } }
讲解:在这个示例中,模板中的文本会被原样输出,包括空格和换行符。模板中的 {{.Name}}
会被替换为数据中的 Name
字段。
动作
注释 action
注释可以在模板中添加不输出的文本,使用 {{/* ... */}}
语法。
注意:
/*
后和*/
前必须有一个空格。
package main import ( "os" "text/template" ) func main() { const templateText = `Hello, {{.Name}}! {{/* This is a comment */}}` tmpl, err := template.New("example").Parse(templateText) if err != nil { panic(err) } data := struct { Name string }{ Name: "go-zero", } err = tmpl.Execute(os.Stdout, data) if err != nil { panic(err) } }
讲解:注释内容不会出现在最终输出中,可以用于在模板中添加说明或备注。
if action
if
action 用于条件判断,如果条件为真,则输出其中的内容。
package main import ( "os" "text/template" ) func main() { const templateText = `{{if .ShowTitle}}<h1>{{.Title}}</h1>{{end}}` tmpl, err := template.New("example").Parse(templateText) if err != nil { panic(err) } data := struct { ShowTitle bool Title string }{ ShowTitle: true, Title: "Hello, go-zero!", } err = tmpl.Execute(os.Stdout, data) if err != nil { panic(err) } }
讲解:if
action 检查 ShowTitle
是否为 true
,如果是,则输出标题。
if-else action
if-else
action 用于条件判断,如果条件为真,则输出 if
部分的内容,否则输出 else
部分的内容。
package main import ( "os" "text/template" ) func main() { const templateText = `{{if .ShowTitle}}<h1>{{.Title}}</h1>{{else}}<h1>No Title</h1>{{end}}` tmpl, err := template.New("example").Parse(templateText) if err != nil { panic(err) } data := struct { ShowTitle bool Title string }{ ShowTitle: false, Title: "Hello, go-zero!", } err = tmpl.Execute(os.Stdout, data) if err != nil { panic(err) } }
讲解:if-else
action 检查 ShowTitle
是否为 true
,如果是,则输出标题,否则输出 No Title
。
if-else-if action
if-else-if
action 用于多个条件的判断。
package main import ( "os" "text/template" ) func main() { const templateText = `{{if .ShowTitle}}<h1>{{.Title}}</h1>{{else if .ShowSubtitle}}<h2>{{.Subtitle}}</h2>{{else}}<p>No Title or Subtitle</p>{{end}}` tmpl, err := template.New("example").Parse(templateText) if err != nil { panic(err) } data := struct { ShowTitle bool ShowSubtitle bool Title string Subtitle string }{ ShowTitle: false, ShowSubtitle: true, Title: "Hello, go-zero!", Subtitle: "Hi, go-zero!", } err = tmpl.Execute(os.Stdout, data) if err != nil { panic(err) } }
讲解:if-else-if
action 检查 ShowTitle
是否为 true
,如果是,则输出标题<h1>{{Hello, go-zero!}}</h1>
;否则检查 ShowSubtitle
是否为 true
,如果是,则输出副标题;否则输出 <h2>Hi, go-zero!</h2>
。
字段链式调用
字段链式调用用于访问嵌套的结构体字段。
package main import ( "os" "text/template" ) func main() { const templateText = `Name: {{.Repo.Name}}, Address: {{.Repo.Address}}` tmpl, err := template.New("example").Parse(templateText) if err != nil { panic(err) } data := struct { Repo struct { Name string Address string } }{ Repo: struct { Name string Address string }{ Name: "go-zero", Address: "https://github.com/zeromicro/go-zero", }, } err = tmpl.Execute(os.Stdout, data) if err != nil { panic(err) } }
讲解:字段链式调用通过 .
操作符访问嵌套结构体中的字段,例如 {{.Repo.Name}}
和 {{.Repo.Address}}
。
二、中级功能
range 数组
range
action 用于遍历数组或切片。
package main import ( "os" "text/template" ) func main() { const templateText = ` <ul> {{range .Projects}}<li>{{.}}</li>{{end}} </ul> ` tmpl, err := template.New("example").Parse(templateText) if err != nil { panic(err) } data := struct { Projects []string }{ Projects: []string{"go-zero", "goctl"}, } err = tmpl.Execute(os.Stdout, data) if err != nil { panic(err) } }
讲解:range
action 遍历 Projects
切片中的每个元素,并生成一个包含每个元素的列表项 (<li>
)。
range map
range
action 也可以用于遍历 map。
package main import ( "os" "text/template" ) func main() { const templateText = ` <ul> {{range $key, $value := .Projects}}<li>{{$key}}: {{$value}}</li>{{end}} </ul> ` tmpl, err := template.New("example").Parse(templateText) if err != nil { panic(err) } data := struct { Projects map[string]string }{ Projects: map[string]string{"Name": "go-zero", "Address": "https://github.com/zeromicro/go-zero"}, } err = tmpl.Execute(os.Stdout, data) if err != nil { panic(err) } }
讲解:range
map action 遍历 Projects
切片中的每个key, value 元素,其变量以 $
开头。
break action
在range
动作中可以通过 break
来中断循环。
package main import ( "os" "text/template" ) func main() { const templateText = ` <ul>{{range .Items}} {{if eq . "Item 3"}}{{break}}{{end}}<li>{{.}}</li>{{end}} </ul>` tmpl, err := template.New("example").Parse(templateText) if err != nil { panic(err) } data := struct { Items []string }{ Items: []string{"Item 1", "Item 2", "Item 3"}, } err = tmpl.Execute(os.Stdout, data) if err != nil { panic(err) } }
讲解:通过{{break}}
,可以打断range循环操作,如上结果输出为
<ul> <li>Item 1</li> <li>Item 2</li> </ul>
continue action
在range
动作中可以通过 continue
来跳过当次循环。
package main import ( "os" "text/template" ) func main() { const templateText = `<ul>{{range .Items}} {{if eq . "Item 2"}}{{continue}}{{else}}<li>{{.}}</li>{{end}}{{end}} </ul>` tmpl, err := template.New("example").Parse(templateText) if err != nil { panic(err) } data := struct { Items []string }{ Items: []string{"Item 1", "Item 2", "Item 3"}, } err = tmpl.Execute(os.Stdout, data) if err != nil { panic(err) } }
讲解:通过 continue
动作,在模板中遇到特定条件时可以跳过当前循环。以上模板输出为
<ul> <li>Item 1</li> <li>Item 3</li> </ul>
子模板
子模板允许将模板划分为多个部分,并在主模板中引用子模板。
package main import ( "os" "text/template" ) func main() { const ( headerTemplate = `{{define "header"}}<html><head><title>{{.Title}}</title></head><body>{{end}}` footerTemplate = `{{define "footer"}}</body></html>{{end}}` bodyTemplate = `{{define "body"}}<h1>{{.Heading}}</h1><p>{{.Content}}</p>{{end}}` mainTemplate = `{{template "header" .}} {{template "body" .}} {{template "footer" .}}` ) tmpl := template.Must(template.New("main").Parse(headerTemplate + footerTemplate + bodyTemplate + mainTemplate)) data := struct { Title string Heading string Content string }{ Title: "Welcome", Heading: "Hello, go-zero!", Content: "This is a simple example of nested templates.", } if err := tmpl.Execute(os.Stdout, data); err != nil { panic(err) } }
讲解:通过定义 header
、footer
和 body
子模板,并在主模板中使用 {{template "header" .}}
等方式引用子模板,可以实现模板的模块化和复用。
with action
with
action 设置一个新的数据上下文,并在该上下文中执行模板。
package main import ( "os" "text/template" ) func main() { const templateText = `{{with .User}}<p>Name: {{.Name}}</p> <p>Age: {{.Age}}</p> {{end}} ` tmpl, err := template.New("example").Parse(templateText) if err != nil { panic(err) } data := struct { User struct { Name string Age int } }{ User: struct { Name string Age int }{ Name: "John", Age: 30, }, } if err := tmpl.Execute(os.Stdout, data); err != nil { panic(err) } }
讲解:with
action 将 User
结构体设置为新的数据上下文{{.}}
,从而简化了模板中对嵌套字段的访问。
三、高级功能
内置函数
Go 模板提供了一些常用的内置函数,可以直接在模板中使用。
package main import ( "os" "text/template" ) func main() { const templateText = ` <p>Upper: {{.Name | upper}}</p> <p>Len: {{len .Name}}</p> ` funcMap := template.FuncMap{ "upper": strings.ToUpper, } tmpl, err := template.New("example").Funcs(funcMap).Parse(templateText) if err != nil { panic(err) } data := struct { Name string }{ Name: "John", } if err := tmpl.Execute(os.Stdout, data); err != nil { panic(err) } }
讲解:示例中使用了 len
内置函数和自定义的 upper
函数。内置函数可以直接在模板中使用,而自定义函数需要通过 template.FuncMap
注册。
自定义函数
自定义函数允许开发者扩展模板的功能。
package main import ( "os" "strings" "text/template" ) func main() { funcMap := template.FuncMap{ "repeat": func(s string, count int) string { return strings.Repeat(s, count) }, } const templateText = `{{repeat .Name 3}}` tmpl, err := template.New("example").Funcs(funcMap).Parse(templateText) if err != nil { panic(err) } data := struct { Name string }{ Name: "Go", } if err := tmpl.Execute(os.Stdout, data); err != nil { panic(err) } }
讲解:自定义函数 repeat
使用 strings.Repeat
函数重复字符串,并通过 template.FuncMap
注册后在模板中使用。
管道
管道 (|) 允许将数据传递给多个函数进行处理。
package main import ( "os" "strings" "text/template" ) func main() { funcMap := template.FuncMap{ "trim": strings.TrimSpace, "upper": strings.ToUpper, } const templateText = `{{.Name | trim | upper}}` tmpl, err := template.New("example").Funcs(funcMap).Parse(templateText) if err != nil { panic(err) } data := struct { Name string }{ Name: " go ", } if err := tmpl.Execute(os.Stdout, data); err != nil { panic(err) } }
讲解:通过管道操作符 |
,数据 {{.Name}}
依次传递给 trim
和 upper
函数进行处理,实现了多步骤的数据处理。
四、综合使用
通过上述基础功能、中级功能和高级功能的介绍,我们可以构建一个功能完整的示例应用。该示例将展示如何使用 text/template
构建一个动态生成 HTML 页面的简单 Web 应用。
package main import ( "html/template" "log" "net/http" "strings" ) // 定义数据结构 type PageData struct { Title string Header string Content string Items []string } // 自定义函数 func trim(str string) string { return strings.TrimSpace(str) } func upper(str string) string { return strings.ToUpper(str) } func main() { // 创建模板函数映射 funcMap := template.FuncMap{ "trim": trim, "upper": upper, } // 定义模板内容 const baseTemplate = ` <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>{{block "title" .}}Default Title{{end}}</title> </head> <body> {{template "header" .}} {{block "content" .}}{{end}} {{template "footer" .}} </body> </html> ` const headerTemplate = ` {{define "header"}} <header> <h1>{{.Header | upper}}</h1> </header> {{end}} ` const footerTemplate = ` {{define "footer"}} <footer> <p>Default Footer Content</p> </footer> {{end}} ` const contentTemplate = ` {{define "content"}} <main> <p>{{.Content}}</p> <ul> {{range .Items}} {{template "item" .}} {{else}} <li>No items found</li> {{end}} </ul> </main> {{end}} ` const itemTemplate = ` {{define "item"}} <li>{{. | trim | upper}}</li> {{end}} ` // 解析所有模板 tmpl := template.Must(template.New("base").Funcs(funcMap).Parse(baseTemplate)) tmpl = template.Must(tmpl.Parse(headerTemplate)) tmpl = template.Must(tmpl.Parse(footerTemplate)) tmpl = template.Must(tmpl.Parse(contentTemplate)) tmpl = template.Must(tmpl.Parse(itemTemplate)) http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { data := PageData{ Title: "Go Template Best Practices", Header: "Welcome to Go Templates", Content: "This is an example demonstrating various features of Go templates.", Items: []string{"Item 1", "Item 2", "Item 3"}, } if err := tmpl.ExecuteTemplate(w, "base", data); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } }) log.Println("Server started at :8080") http.ListenAndServe(":8080", nil) }
主程序 (main.go
)
- 定义数据结构:
PageData
结构体用于传递数据给模板。 - 自定义函数:定义了
trim
和upper
两个自定义函数,用于字符串处理。 - 创建模板函数映射:使用
template.FuncMap
创建函数映射,以便在模板中使用自定义函数。 - 定义模板内容:将模板内容作为字符串嵌入到 Go 代码中,包括基础模板和各个子模板。
- 解析所有模板:使用
template.Must
和template.New
解析所有模板字符串,并将函数映射添加到模板。 - 处理 HTTP 请求:在 HTTP 处理函数中,创建
PageData
实例并传递给模板,通过ExecuteTemplate
方法渲染模板并输出到浏览器。
基础模板 (baseTemplate
)
block
动作:使用block
动作定义可重写的块,如title
和content
。子模板可以重写这些块以实现模板继承。template
动作:使用template
动作包含其他模板(如header
和footer
),实现模板的模块化和复用。
头部模板 (headerTemplate
)
- 定义模板:使用
define
动作定义一个可重用的模板块header
。 - 管道操作符:使用管道操作符将
Header
字段的值传递给upper
函数,转换为大写。
页脚模板 (footerTemplate
)
- 定义模板:定义一个简单的页脚模板,包含固定的内容。
内容模板 (contentTemplate
)
range
动作:使用range
动作遍历Items
列表,为每个项目渲染item
模板。如果列表为空,则显示else
分支中的内容。- 包含模板:使用
template
动作包含item
模板,实现列表项的模块化渲染。
列表项模板 (itemTemplate
)
- 定义模板:定义一个用于渲染单个列表项的模板。
- 管道操作符:使用管道操作符将列表项值依次传递给
trim
和upper
函数,去除空格并转换为大写。
运行示例
- 将上述代码保存到
main.go
文件中。 - 运行
main.go
程序。 - 打开浏览器并访问
http://localhost:8080
,查看渲染结果。
五、实际应用
最近在写一个 goctl web 的应用来构造 api 文件,通过前端 form 表单数据来对 API 模板进行数据渲染,模板内容如下:
// generated by goctl. syntax = "v1" {{.types}} {{range $group := .groups}}{{/* range route groups */}} {{/* generate @server block */}} {{with $group.server}}@server( {{if .jwt}}jwt: JWTAuth{{end}} {{if .prefix}}prefix: {{.prefix}}{{end}} {{if .group}}group: {{.group}}{{end}} {{if .timeout}}timeout: {{.timeout}}{{end}} {{if .middleware}}middleware: {{.middleware}}{{end}} {{if .maxBytes}}maxBytes: {{.maxBytes}}{{end}} ){{end}} {{/* generate service block */}} {{with $group.service}}service {{.name}}{ {{ $routes := .routes}} {{/* define a variable to block the follows range block */}} {{range $idx, $route := .routes}}{{/* releace $route to dot */}} @handler {{$route.handlerName}} {{$route.method}} {{$route.path}} {{if $route.request}}({{$route.request}}){{end}} {{if $route.response}}returns ({{$route.response}}){{end}}{{if lessThan $idx (len $routes)}} {{end}} {{end}}}{{end}} {{end}}
模板头部
// generated by goctl. syntax = "v1" {{.types}}
// generated by goctl.
:注释,表示这个文件是由 goctl
工具生成的。
syntax = "v1"
:定义了语法版本为 v1
。
{{.types}}
:插入模板上下文中的结构体数据。
遍历分组(range
)
{{range $group := .groups}}{{/* range route groups */}}
{{range $group := .groups}}
:遍历模板上下文中的 groups
路由分组列表,每个路由分组 group
被赋值给变量 $group
。
生成 @server
块
{{with $group.server}}@server( {{if .jwt}}jwt: JWTAuth{{end}} {{if .prefix}}prefix: {{.prefix}}{{end}} {{if .group}}group: {{.group}}{{end}} {{if .timeout}}timeout: {{.timeout}}{{end}} {{if .middleware}}middleware: {{.middleware}}{{end}} {{if .maxBytes}}maxBytes: {{.maxBytes}}{{end}} ){{end}}
{{with $group.server}}
:进入 $group.server
子模板上下文,将$group.server
重新赋值到{{.}}
,减少冗长的链式调用。
{{if .jwt}}jwt: JWTAuth{{end}}
:如果 jwt
存在,则生成 jwt: JWTAuth
,这里用到了条件动作,其他几个 if
条件类似。
{{end}}
:结束 with
动作。
生成 service
块
{{/* generate service block */}} {{with $group.service}}service {{.name}}{ {{ $routes := .routes}} {{/* define a variable to block the follows range block */}}
{{
/\* generate service block \*/
}}
用到了注释动作,记得注释的/*
后和 */
要有空格。
{{with $group.service}}
:进入 $group.service
子模板上下文,将 $group.service
赋值到{{.}}
,减少冗长的链式调用。
service {{.name}}{
:生成 service <name>{
,其中 <name>
是 service
的名称。
{{ $routes := .routes}}
:定义一个临时变量 $routes
保存 routes
列表,用于突破下文的 range 上下文。
遍历 routes
列表并生成每个路由
{{range $idx, $route := .routes}}{{/* releace $route to dot */}} @handler {{$route.handlerName}} {{$route.method}} {{$route.path}} {{if $route.request}}({{$route.request}}){{end}} {{if $route.response}}returns ({{$route.response}}){{end}}{{if lessThan $idx (len $routes)}} {{end}} {{end}}}{{end}}
{{range $idx, $route := .routes}}
:遍历 routes
列表,每个 route
被赋值给 $route
,索引被赋值给 $idx
。
@handler {{$route.handlerName}}
:生成 @handler <handlerName>
,其中 <handlerName>
是处理器名称。
{{$route.method}} {{$route.path}}
:生成 <method> <path>
,其中 <method>
是 HTTP 方法,<path>
是路径。
{{if $route.request}}({{$route.request}}){{end}}
:如果 request
存在,则生成 (<request>)
。
{{if $route.response}}returns ({{$route.response}}){{end}}
:如果 response
存在,则生成 returns (<response>)
。
{{if lessThan $idx (len $routes)}}
:检查当前索引是否小于 routes
列表的长度。lessThan
用到自定义函数功能。
如下是模板数据填充的部分代码
var data []KV for _, group := range mergedReq.List { var groupData = KV{} var hasServer bool var server = KV{} if group.Jwt { hasServer = true server["jwt"] = group.Jwt } if len(group.Prefix) > 0 { hasServer = true server["prefix"] = group.Prefix } if len(group.Group) > 0 { hasServer = true server["group"] = group.Group } if group.Timeout > 0 { hasServer = true server["timeout"] = fmt.Sprintf("%dms", group.Timeout) } if len(group.Middleware) > 0 { hasServer = true server["middleware"] = group.Middleware } if group.MaxBytes > 0 { hasServer = true server["maxBytes"] = group.MaxBytes } if hasServer { groupData["server"] = server } var routesData []KV for _, route := range group.Routes { var request, response string if len(route.RequestBody) > 0 { request = l.generateTypeName(route, true) } if !util.IsEmptyStringOrWhiteSpace(route.ResponseBody) { response = l.generateTypeName(route, false) } routesData = append(routesData, KV{ "handlerName": l.generateHandlerName(route), "method": strings.ToLower(route.Method), "path": route.Path, "request": request, "response": response, }) } var service = KV{ "name": req.Name, "routes": routesData, } groupData["service"] = service data = append(data, groupData) } t, err := template.New("api").Funcs(map[string]any{ "lessThan": func(idx int, length int) bool { return idx < length-1 }, }).Parse(apiTemplate) if err != nil { return nil, err } tps, err := l.generateTypes(mergedReq.List) if err != nil { return nil, err } var typeString string if len(tps) > 0 { typeString = strings.Join(tps, "\n\n") } w := bytes.NewBuffer(nil) err = t.Execute(w, map[string]any{ "types": typeString, "groups": data, }) if err != nil { return nil, err } formatWriter := bytes.NewBuffer(nil) err = format.Source(w.Bytes(), formatWriter) if err != nil { return nil, err }
最后在 web 页面上展示如图,图中右边的 api 内容就是由模板渲染出来的。
六、总结
本文介绍了 Go 语言中 text/template
包的基础功能、中级功能和高级功能,并通过具体示例讲解了每个功能的使用方法。通过这些示例,我们可以看到 text/template
包的强大功能以及在实际开发中的广泛应用。希望本文能帮助您更好地理解和使用 text/template
,构建出更加灵活和高效的数据驱动应用。