Golang利用Template模板動態(tài)生成文本
Go語言中的Go Template是一種用于生成文本輸出的簡單而強大的模板引擎。它提供了一種靈活的方式來生成各種格式的文本,例如HTML、XML、JSON等。
Go Template的具有以下主要特性:
- 簡潔易用:Go Template語法簡潔而易于理解。它使用一對雙大括號“{{}}”來標記模板的占位符和控制結(jié)構(gòu)。這種簡單的語法使得模板的編寫和維護變得非常方便。
- 數(shù)據(jù)驅(qū)動:Go Template支持數(shù)據(jù)驅(qū)動的模板生成。你可以將數(shù)據(jù)結(jié)構(gòu)傳遞給模板,并在模板中使用點號“.”來引用數(shù)據(jù)的字段和方法。這種數(shù)據(jù)驅(qū)動的方式使得模板可以根據(jù)不同的數(shù)據(jù)動態(tài)生成輸出。
- 條件和循環(huán):Go Template提供了條件語句和循環(huán)語句,使得你可以根據(jù)條件和迭代來控制模板的輸出。你可以使用“if”、“else”、“range”等關(guān)鍵字來實現(xiàn)條件判斷和循環(huán)迭代,從而生成靈活的輸出。
- 過濾器和函數(shù):Go Template支持過濾器和函數(shù),用于對數(shù)據(jù)進行轉(zhuǎn)換和處理。你可以使用內(nèi)置的過濾器來格式化數(shù)據(jù),例如日期格式化、字符串截斷等。此外,你還可以定義自己的函數(shù),并在模板中調(diào)用這些函數(shù)來實現(xiàn)更復(fù)雜的邏輯和操作。
- 嵌套模板:Go Template支持模板的嵌套,允許你在一個模板中包含其他模板。這種模板的組合和嵌套機制可以幫助你構(gòu)建更大型、更復(fù)雜的模板結(jié)構(gòu),提高代碼的可重用性和可維護性。
在很多Go開發(fā)的工具,項目都大量的使用了template模板。例如: Helm,K8s,Prometheus,以及一些code-gen代碼生成器等等。Go template提供了一種模板機制,通過預(yù)聲明模板,傳入自定義數(shù)據(jù)來靈活的定制各種文本。
1.示例
我們通過一個示例來了解一下template的基本使用。
首先聲明一段模板
var md = `Hello,{{ . }}`
解析模板并執(zhí)行
func main() { tpl := template.Must(template.New("first").Parse(md)) if err := tpl.Execute(os.Stdout, "Jack"); err != nil { log.Fatal(err) } } // 輸出 // Hello Jack
在上述例子中, {{ . }}
前后花括號屬于分界符,template會對分界符內(nèi)的數(shù)據(jù)進行解析填充。其中 .
代表當前對象,這種概念在很多語言中都存在。
在main函數(shù)中,我們通過template.New
創(chuàng)建一個名為"first"的template,并用此template進行Parse解析模板。隨后,再進行執(zhí)行:傳入io.Writer,data
,template會將數(shù)據(jù)填充至解析的模板中,再輸出到傳入的io.Writer上。
我們再來看一個例子
// {{ .xxoo -}} 刪除右側(cè)的空白 var md = `個人信息: 姓名: {{ .Name }} 年齡: {{ .Age }} 愛好: {{ .Hobby -}} ` type People struct { Name string Age int } func (p People) Hobby() string { return "唱,跳,rap,籃球" } func main() { tpl := template.Must(template.New("first").Parse(md)) p := People{ Name: "Jackson", Age: 20, } if err := tpl.Execute(os.Stdout, p); err != nil { log.Fatal(err) } } // 輸出 //個人信息: //姓名: Jackson //年齡: 20 //愛好: 唱,跳,rap,籃球
Hobby屬于People的方法,所以在模板中也可以通過.
進行調(diào)用。需要注意: 不管是字段還是方法,由于template實際解析的包與當前包不同,無論是字段還是方法必須是導(dǎo)出的。
在template中解析時,它 移除了 {{
和 }}
里面的內(nèi)容,但是留下的空白完全保持原樣。所以解析出來的時候,我們需要對空白進行控制。YAML認為空白是有意義的,因此管理空白變得很重要。我們可以通過-
進行控制空白。
{{-
(包括添加的橫杠和空格)表示向左刪除空白, 而 -}}
表示右邊的空格應(yīng)該被去掉。
要確保-
和其他命令之間有一個空格。
{{- 10 }}: "表示向左刪除空格,打印10"
{{ -10 }}: "表示打印-10"
2.流程控制
條件判斷 IF ELSE
在template中,提供了if/else
的流程判斷。
我們看一下doc的定義:
{{if pipeline}} T1 {{end}}
如果 pipeline 的值為空,則不生成輸出;
否則,執(zhí)行T1。空值為 false、0、任何
nil 指針或接口值,以及
長度為零的任何數(shù)組、切片、映射或字符串。
點不受影響。
{{if pipeline}} T1 {{else}} T0 {{end}}
如果 pipeline 的值為空,則執(zhí)行 T0;
否則,執(zhí)行T1。點不受影響。
{{if pipeline}} T1 {{else if pipeline}} T0 {{end}}
為了簡化 if-else 鏈的外觀,
if 的 else 操作可以直接包含另一個 if
其中pipeline命令是一個簡單的值(參數(shù))或一個函數(shù)或方法調(diào)用。我們第一個例子的hobby就屬于方法調(diào)用。
繼續(xù)是上面的案例,我們添加了一個IF/ELSE來判斷年齡,在IF中我們使用了一個內(nèi)置函數(shù)gt
判斷年齡。
在template中,調(diào)用函數(shù),傳遞參數(shù)是跟在函數(shù)后面: function arg1 agr2
。
或者也可以通過管道符進行傳遞:arg | function
每個函數(shù)都必須有1到2個返回值,如果有2個則后一個必須是error接口類型。
var md = `個人信息: 姓名: {{ .Name }} 年齡: {{ .Age }} 愛好: {{ .Hobby -}} {{ if gt .Age 18 }} 成年人 {{ .Age | print }} {{ else }} 未成年人 {{ end }} ` // 輸出 //個人信息: //姓名: Jackson //年齡: 20 //愛好: 唱,跳,rap,籃球 //成年人 //20
循環(huán)控制range
template同時也提供了循環(huán)控制的功能。我們還是先看一下doc
{range pipeline}} T1 {{end}} pipeline 的值必須是數(shù)組、切片、映射或通道。
如果管道的值長度為零,則不輸出任何內(nèi)容;
否則,將點設(shè)置為數(shù)組的連續(xù)元素,
切片或映射并執(zhí)行 T1。如果值是映射并且鍵是具有定義順序的基本類型,則將按排序鍵順序訪問
{{range pipeline}} T1 {{else}} T0 {{end}}
pipeline 的值必須是數(shù)組、切片、映射或通道。
如果管道的值長度為零,則 . 不受影響并
執(zhí)行 T0;否則,將 . 設(shè)置為數(shù)組、切片或映射的連續(xù)元素,并執(zhí)行 T1。
{{break}}
最里面的 {{range pipeline}} 循環(huán)提前結(jié)束,停止當前迭代并繞過所有剩余迭代。
{{continue}}
最里面的 {{range pipeline}} 循環(huán)的跳過當前迭代
整合上面的IF/ELSE,我們做一個綜合案例
var md = ` Start iteration: {{- range . }} {{- if gt . 3 }} 超過3 {{- else }} {{ . }} {{- end }} {{ end }} ` func main() { tpl := template.Must(template.New("first").Parse(md)) p := []int{1, 2, 3, 4, 5, 6} if err := tpl.Execute(os.Stdout, p); err != nil { log.Fatal(err) } } // 輸出 //1 //2 //3 //超過3 //超過3 //超過3
我們通過{{ range . }}
遍歷傳入的對象,在循環(huán)內(nèi)部再通過{{ if }}/{{ else }}
判斷每個元素的大小。
作用域控制with
在語言中都有一個作用域的概念。template也提供了通過使用with去修改作用域。
我們來看一個案例
var md = ` people name(out scope): {{ .Name }} dog name(out scope): {{ .MyDog.Name }} {{- with .MyDog }} dog name(in scope): {{ .Name }} people name(in scope): {{ $.Name }} {{ end }} ` type People struct { Name string Age int MyDog Dog } type Dog struct { Name string } func main() { tpl := template.Must(template.New("first").Parse(md)) p := People{Name: "Lucy", MyDog: Dog{Name: "Tom"}} if err := tpl.Execute(os.Stdout, p); err != nil { log.Fatal(err) } } // 輸出 //people name(out scope): Lucy //dog name(out scope): Tom //dog name(in scope): Tom //people name(in scope): Lucy
在頂層作用域中,我們直接可以通過.
去獲取對象的信息。在聲明的with
中,我們將頂層對象的MyDog傳入,那么在with作用域中,通過.
獲取的對象就是Dog。所以在with
中我們可以直接通過.
獲取Dog的name。
有些時候,在子作用域中我們可能也希望可以獲取到頂層對象,那么我們可以通過$
獲取頂層對象。上述例子的$.
獲取到People。
3.函數(shù)
在第二節(jié)內(nèi)容中,我們使用了print,gt
函數(shù),這些函數(shù)都是預(yù)定義在template中。我們通過查閱源碼可以查看預(yù)定義了以下函數(shù):
func builtins() FuncMap { return FuncMap{ "and": and, "call": call, "html": HTMLEscaper, "index": index, "slice": slice, "js": JSEscaper, "len": length, "not": not, "or": or, "print": fmt.Sprint, "printf": fmt.Sprintf, "println": fmt.Sprintln, "urlquery": URLQueryEscaper, // Comparisons "eq": eq, // == "ge": ge, // >= "gt": gt, // > "le": le, // <= "lt": lt, // < "ne": ne, // != } }
在實際開發(fā)中,僅僅是這些函數(shù)是很難滿足我們的需求。此時,我們希望能夠傳入自定義函數(shù),在我們編寫模板的時候可以使用自定義的函數(shù)。
我們引入一個需求: 希望將傳入的str可以轉(zhuǎn)為小寫。
var md = ` result: {{ . | lower }} ` func Lower(str string) string { return strings.ToLower(str) } func main() { tpl := template.Must(template.New("demo").Funcs(map[string]any{ "lower": Lower, }).Parse(md)) tpl.Execute(os.Stdout, "HELLO FOSHAN") } // 輸出 // result: hello foshan
由于template支持鏈式調(diào)用,所以我們一般把Parse放在最后
我們通過調(diào)用Funcs
,傳入functionName : function
的map。
執(zhí)行模板時,函數(shù)從兩個函數(shù)map中查找:首先是模板函數(shù)map,然后是全局函數(shù)map。一般不在模板內(nèi)定義函數(shù),而是使用Funcs方法添加函數(shù)到模板里。
方法必須有一到兩個返回值,如果是兩個,那么第二個一定是error接口類型
注意:Funcs
必須在解析parse前調(diào)用。如果模板已經(jīng)解析了,再傳入funcs,template并不知道該函數(shù)應(yīng)該如何映射。
4.變量
函數(shù)、管道符、對象和控制結(jié)構(gòu)都可以控制,我們轉(zhuǎn)向很多編程語言中更基本的思想之一:變量。 在模板中,很少被使用。但是我們可以使用變量簡化代碼,并更好地使用with
和range
。
我們通過{{ $var := .Obj }}
聲明變量,在with/range
中我們使用的會比較頻繁
var md = ` {{- $count := len . -}} 共有{{ $count }}個元素 {{- range $k,$v := . }} {{ $k }} => {{ $v }} {{- end }} ` func main() { tpl := template.Must(template.New("demo").Parse(md)) tpl.Execute(os.Stdout, map[string]string{ "p1": "Jack", "p2": "Tom", "p3": "Lucy", }) } // 輸出 // 共有3個元素 // p1 => Jack // p2 => Tom // p3 => Lucy
{{ var }}聲明的變量也有作用域的概念,如果在頂層作用域中聲明了var,那么在內(nèi)部作用域可以直接通過獲取該變量
我們通過{{- range $k,$v := . }}
遍歷map中每一個KV,這種寫法類似于Golang的for-range。
5.命名模板
在Go語言的模板引擎中,命名模板是指通過給模板賦予一個唯一的名稱,將其存儲在模板集中,以便后續(xù)可以通過該名稱來引用和執(zhí)行該模板。
通過使用命名模板,你可以將一組相關(guān)的模板邏輯組織在一起,并在需要的時候方便地調(diào)用和重用它們。這對于構(gòu)建復(fù)雜的模板結(jié)構(gòu)和提高模板的可維護性非常有用。
在編寫復(fù)雜模板的時候,我們總是希望可以抽象出公用模板,那么此時就需要使用命名模板進行復(fù)用。
本節(jié)將基于K8sPod模板的案例來學(xué)習如何使用命名模板進行抽象復(fù)用。
我們看一下doc
{{template "name"}}
具有指定名稱的模板以無數(shù)據(jù)執(zhí)行。
{{template "name" pipeline}}
具有指定名稱的模板以pipeline結(jié)果執(zhí)行。
通過define定義模板名稱
{{ define "container" }}
模板
{{ end }}
通過template使用模板
{{ template "container" }}
我們在使用template.New傳入的name,實際上就是定義了模板的名稱
案例:我們希望抽象出Pod的container,通過代碼來傳入數(shù)據(jù)生成container,避免重復(fù)的編寫yaml。
var pod = ` apiVersion: v1 kind: Pod metadata: name: "test" spec: containers: {{- template "container" .}} ` var container = ` {{ define "container" }} - name: {{ .Name }} image: "{{ .Image}}" {{ end }} ` func main() { tpl := template.Must(template.New("demo").Parse(pod)) tpl.Parse(container) tpl.ExecuteTemplate(os.Stdout, "demo", struct { Name string Image string }{ "nginx", "1.14.1", }) } // 輸出 apiVersion: v1 kind: Pod metadata: name: "test" spec: containers: - name: nginx image: "1.14.1"
tpl可以解析多個模板,在不同模板中通過define定義模板即可。使用ExecuteTemplate傳入模板名指定解析模板。在{{- template "container" .}}
中可以傳入對象數(shù)據(jù)。
在實際開發(fā)中,我們往往不會采用打印的方式輸出??梢愿鶕?jù)不同的需求,在Execute執(zhí)行時選擇不同的io.Writer
。往往我們更希望寫入到文件中。
6.Template常用函數(shù)
func Must(t *Template, err error) *Template
Must是一個helper函數(shù),它封裝對返回(Template, error)的函數(shù)的調(diào)用,并在錯誤非nil時panic。它旨在用于template初始化。
// 解析指定文件 // 示例: ParseFiles(./pod.tpl) func ParseFiles(filenames ...string) (*Template, error) // 解析filepath.Match匹配文件 // 示例: ParseGlob(/data/*.tpl) func ParseGlob(pattern string) (*Template, error)
這兩個函數(shù)幫助我們解析文件中的模板,大多數(shù)情況下我們都是將模板寫在.tpl
結(jié)尾的文件中。通過不同的解析規(guī)則解析對應(yīng)的文件。
func (t *Template) Templates() []*Template
返回當前t相關(guān)的模板的slice,包括t本身。
func (t *Template) ExecuteTemplate(wr io.Writer, name string, data any) error
傳入模板名稱,執(zhí)行指定的模板。
如果在執(zhí)行模板或?qū)懭肫漭敵鰰r發(fā)生錯誤,執(zhí)行將停止,但部分結(jié)果可能已經(jīng)被寫入輸出寫入器。模板可以安全地并行執(zhí)行,但如果并行執(zhí)行共享一個Writer,則輸出可能交錯。
func (t *Template) Delims(left, right string) *Template
修改模板中的分界符,可以將{{}}
修改為<>
func (t *Template) Clone() (*Template, error)
clone返回模板的副本,包括所有關(guān)聯(lián)模板。在clone的副本上添加模板是不會影響原始模板的。所以我們可以將其用于公共模板,通過clone獲取不同的副本。
7.總結(jié)
Golang的template提高代碼重用性:模板引擎允許你創(chuàng)建可重用的模板片段。通過將重復(fù)的模板邏輯提取到單獨的模板中,并在需要時進行調(diào)用,可以減少代碼重復(fù),提高代碼的可維護性和可擴展性。有許多code-gen使用了template + cobra方式生成復(fù)用代碼和模板代碼,有利于我們解放雙手。
以上就是Golang利用Template模板動態(tài)生成文本的詳細內(nèi)容,更多關(guān)于Go Template的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Golang哈希算法實現(xiàn)配置文件的監(jiān)控功能詳解
這篇文章主要介紹了Golang哈希算法實現(xiàn)配置文件的監(jiān)控功能,哈希和加密類似,唯一區(qū)別是哈希是單項的,即哈希后的數(shù)據(jù)無法解密,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習或者工作具有一定的參考學(xué)習價值,需要的朋友們下面隨著小編來一起學(xué)習吧2023-03-03vscode配置go開發(fā)環(huán)境的實戰(zhàn)過程
vscode配置go的開發(fā)環(huán)境很簡單,下面這篇文章主要給大家介紹了關(guān)于vscode配置go開發(fā)環(huán)境的實戰(zhàn)過程,文中通過實例代碼介紹的非常詳細,需要的朋友可以參考下2022-06-0610個現(xiàn)代網(wǎng)站開發(fā)必備的Go軟件包工具盤點
這篇文章主要為大家介紹了10個現(xiàn)代網(wǎng)站開發(fā)必備的Go軟件包,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-10-10Golang基礎(chǔ)常識性面試中常見的六大陷阱及應(yīng)對技巧總結(jié)
Go是一門簡單有趣的語言,但與其他語言類似,它會有一些技巧,這篇文章主要給大家介紹了關(guān)于Golang基礎(chǔ)常識性面試中常見的六大陷阱及應(yīng)對技巧的相關(guān)資料,文中通過代碼介紹的非常詳細,需要的朋友可以參考下2024-08-08