Goctl 技術系列 - 通過模板簡化應用開發(fā)
在現(xiàn)代軟件開發(fā)中,數(shù)據(jù)驅(qū)動的應用程序逐漸成為主流。無論是構建動態(tài)網(wǎng)站、代碼生成,生成配置文件,還是創(chuàng)建復雜的文檔模板,數(shù)據(jù)驅(qū)動的方式都能顯著提升開發(fā)效率和代碼可維護性。在 Go 語言中,text/template 包提供了一種強大的方式來處理文本和數(shù)據(jù)的結合。
一、基礎功能回顧
文本和空格
在 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}} 會被替換為數(shù)據(jù)中的 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)
}
}
講解:注釋內(nèi)容不會出現(xiàn)在最終輸出中,可以用于在模板中添加說明或備注。
if action
if action 用于條件判斷,如果條件為真,則輸出其中的內(nèi)容。
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 部分的內(nèi)容,否則輸出 else 部分的內(nèi)容。
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>。
字段鏈式調(diào)用
字段鏈式調(diào)用用于訪問嵌套的結構體字段。
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)
}
}
講解:字段鏈式調(diào)用通過 . 操作符訪問嵌套結構體中的字段,例如 {{.Repo.Name}} 和 {{.Repo.Address}}。
二、中級功能
range 數(shù)組
range action 用于遍歷數(shù)組或切片。
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 來中斷循環(huán)。
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循環(huán)操作,如上結果輸出為
<ul>
<li>Item 1</li>
<li>Item 2</li>
</ul>
continue action
在range動作中可以通過 continue 來跳過當次循環(huán)。
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 動作,在模板中遇到特定條件時可以跳過當前循環(huán)。以上模板輸出為
<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" .}} 等方式引用子模板,可以實現(xiàn)模板的模塊化和復用。
with action
with action 設置一個新的數(shù)據(jù)上下文,并在該上下文中執(zhí)行模板。
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 結構體設置為新的數(shù)據(jù)上下文{{.}},從而簡化了模板中對嵌套字段的訪問。
三、高級功能
內(nèi)置函數(shù)
Go 模板提供了一些常用的內(nèi)置函數(shù),可以直接在模板中使用。
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 內(nèi)置函數(shù)和自定義的 upper 函數(shù)。內(nèi)置函數(shù)可以直接在模板中使用,而自定義函數(shù)需要通過 template.FuncMap 注冊。
自定義函數(shù)
自定義函數(shù)允許開發(fā)者擴展模板的功能。
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)
}
}
講解:自定義函數(shù) repeat 使用 strings.Repeat 函數(shù)重復字符串,并通過 template.FuncMap 注冊后在模板中使用。
管道
管道 (|) 允許將數(shù)據(jù)傳遞給多個函數(shù)進行處理。
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)
}
}
講解:通過管道操作符 |,數(shù)據(jù) {{.Name}} 依次傳遞給 trim 和 upper 函數(shù)進行處理,實現(xiàn)了多步驟的數(shù)據(jù)處理。
四、綜合使用
通過上述基礎功能、中級功能和高級功能的介紹,我們可以構建一個功能完整的示例應用。該示例將展示如何使用 text/template 構建一個動態(tài)生成 HTML 頁面的簡單 Web 應用。
package main
import (
"html/template"
"log"
"net/http"
"strings"
)
// 定義數(shù)據(jù)結構
type PageData struct {
Title string
Header string
Content string
Items []string
}
// 自定義函數(shù)
func trim(str string) string {
return strings.TrimSpace(str)
}
func upper(str string) string {
return strings.ToUpper(str)
}
func main() {
// 創(chuàng)建模板函數(shù)映射
funcMap := template.FuncMap{
"trim": trim,
"upper": upper,
}
// 定義模板內(nèi)容
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)
- 定義數(shù)據(jù)結構:PageData 結構體用于傳遞數(shù)據(jù)給模板。
- 自定義函數(shù):定義了 trim 和 upper 兩個自定義函數(shù),用于字符串處理。
- 創(chuàng)建模板函數(shù)映射:使用 template.FuncMap 創(chuàng)建函數(shù)映射,以便在模板中使用自定義函數(shù)。
- 定義模板內(nèi)容:將模板內(nèi)容作為字符串嵌入到 Go 代碼中,包括基礎模板和各個子模板。
- 解析所有模板:使用 template.Must 和 template.New 解析所有模板字符串,并將函數(shù)映射添加到模板。
- 處理 HTTP 請求:在 HTTP 處理函數(shù)中,創(chuàng)建 PageData 實例并傳遞給模板,通過 ExecuteTemplate 方法渲染模板并輸出到瀏覽器。
基礎模板 (baseTemplate)
- block 動作:使用 block 動作定義可重寫的塊,如 title 和 content。子模板可以重寫這些塊以實現(xiàn)模板繼承。
- template 動作:使用 template 動作包含其他模板(如 header 和 footer),實現(xiàn)模板的模塊化和復用。
頭部模板 (headerTemplate)
- 定義模板:使用 define 動作定義一個可重用的模板塊 header。
- 管道操作符:使用管道操作符將 Header 字段的值傳遞給 upper 函數(shù),轉(zhuǎn)換為大寫。
頁腳模板 (footerTemplate)
- 定義模板:定義一個簡單的頁腳模板,包含固定的內(nèi)容。
內(nèi)容模板 (contentTemplate)
- range 動作:使用 range 動作遍歷 Items 列表,為每個項目渲染 item 模板。如果列表為空,則顯示 else 分支中的內(nèi)容。
- 包含模板:使用 template 動作包含 item 模板,實現(xiàn)列表項的模塊化渲染。
列表項模板 (itemTemplate)
- 定義模板:定義一個用于渲染單個列表項的模板。
- 管道操作符:使用管道操作符將列表項值依次傳遞給 trim 和 upper 函數(shù),去除空格并轉(zhuǎn)換為大寫。
運行示例
- 將上述代碼保存到 main.go 文件中。
- 運行 main.go 程序。
- 打開瀏覽器并訪問 http://localhost:8080,查看渲染結果。
五、實際應用
最近在寫一個 goctl web 的應用來構造 api 文件,通過前端 form 表單數(shù)據(jù)來對 API 模板進行數(shù)據(jù)渲染,模板內(nèi)容如下:
// 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}}:插入模板上下文中的結構體數(shù)據(jù)。
遍歷分組(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重新賦值到{{.}},減少冗長的鏈式調(diào)用。
{{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 賦值到{{.}},減少冗長的鏈式調(diào)用。
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 用到自定義函數(shù)功能。
如下是模板數(shù)據(jù)填充的部分代碼:
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 內(nèi)容就是由模板渲染出來的。
https://gen.go-zero.dev/
圖片
六、總結
本文介紹了 Go 語言中 text/template 包的基礎功能、中級功能和高級功能,并通過具體示例講解了每個功能的使用方法。通過這些示例,我們可以看到 text/template 包的強大功能以及在實際開發(fā)中的廣泛應用。希望本文能幫助您更好地理解和使用 text/template,構建出更加靈活和高效的數(shù)據(jù)驅(qū)動應用。
項目地址
https://github.com/zeromicro/go-zero