你知道Golang的模板怎麼用嗎?帶你瞭解動態文字的生成!

發表於2023-09-18

Golang Template

Go語言中的Go Template是一種用於生成文字輸出的簡單而強大的模板引擎。它提供了一種靈活的方式來生成各種格式的文字,例如HTML、XML、JSON等。

Go Template的具有以下主要特性:

  1. 簡潔易用:Go Template語法簡潔而易於理解。它使用一對雙大括號“{{}}”來標記模板的佔位符和控制結構。這種簡單的語法使得模板的編寫和維護變得非常方便。
  2. 資料驅動:Go Template支援資料驅動的模板生成。你可以將資料結構傳遞給模板,並在模板中使用點號“.”來引用資料的欄位和方法。這種資料驅動的方式使得模板可以根據不同的資料動態生成輸出。
  3. 條件和迴圈:Go Template提供了條件語句和迴圈語句,使得你可以根據條件和迭代來控制模板的輸出。你可以使用“if”、“else”、“range”等關鍵字來實現條件判斷和迴圈迭代,從而生成靈活的輸出。
  4. 過濾器和函式:Go Template支援過濾器和函式,用於對資料進行轉換和處理。你可以使用內建的過濾器來格式化資料,例如日期格式化、字串截斷等。此外,你還可以定義自己的函式,並在模板中呼叫這些函式來實現更復雜的邏輯和操作。
  5. 巢狀模板:Go Template支援模板的巢狀,允許你在一個模板中包含其他模板。這種模板的組合和巢狀機制可以幫助你構建更大型、更復雜的模板結構,提高程式碼的可重用性和可維護性。

在很多Go開發的工具,專案都大量的使用了template模板。例如: Helm,K8s,Prometheus,以及一些code-gen程式碼生成器等等。Go template提供了一種模板機制,透過預宣告模板,傳入自定義資料來靈活的定製各種文字。

1.示例

我們透過一個示例來瞭解一下template的基本使用。

首先宣告一段模板

var md = `Hello,{{ . }}`

解析模板並執行

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會對分界符內的資料進行解析填充。其中 . 代表當前物件,這種概念在很多語言中都存在。

在main函式中,我們透過template.New建立一個名為"first"的template,並用此template進行Parse解析模板。隨後,再進行執行:傳入io.Writer,data,template會將資料填充至解析的模板中,再輸出到傳入的io.Writer上。

我們再來看一個例子

// {{ .xxoo -}} 刪除右側的空白
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的方法,所以在模板中也可以透過.進行呼叫。需要注意: 不管是欄位還是方法,由於template實際解析的包與當前包不同,無論是欄位還是方法必須是匯出的。

在template中解析時,它 移除了 {{}} 裡面的內容,但是留下的空白完全保持原樣。所以解析出來的時候,我們需要對空白進行控制。YAML認為空白是有意義的,因此管理空白變得很重要。我們可以透過-進行控制空白。

{{- (包括新增的橫槓和空格)表示向左刪除空白, 而 -}} 表示右邊的空格應該被去掉。

要確保-和其他命令之間有一個空格。

{{- 10 }}: "表示向左刪除空格,列印10"

{{ -10 }}: "表示列印-10"

2.流程控制

條件判斷 IF ELSE

在template中,提供了if/else的流程判斷。

我們看一下doc的定義:

{{if pipeline}} T1 {{end}}
    如果 pipeline 的值為空,則不生成輸出;
    否則,執行T1。空值為 false、0、任何
    nil 指標或介面值,以及
    長度為零的任何陣列、切片、對映或字串。
    點不受影響。
{{if pipeline}} T1 {{else}} T0 {{end}}
    如果 pipeline 的值為空,則執行 T0;
    否則,執行T1。點不受影響。
{{if pipeline}} T1 {{else if pipeline}} T0 {{end}}
    為了簡化 if-else 鏈的外觀,
    if 的 else 操作可以直接包含另一個 if

其中pipeline命令是一個簡單的值(引數)或一個函式或方法呼叫。我們第一個例子的hobby就屬於方法呼叫。

繼續是上面的案例,我們新增了一個IF/ELSE來判斷年齡,在IF中我們使用了一個內建函式gt判斷年齡。

在template中,呼叫函式,傳遞引數是跟在函式後面: function arg1 agr2

或者也可以透過管道符進行傳遞:arg | function

每個函式都必須有1到2個返回值,如果有2個則後一個必須是error介面型別。

var md = `個人資訊:
姓名: {{ .Name }}
年齡: {{ .Age }}
愛好: {{ .Hobby -}}
{{ if gt .Age 18 }}
成年人
{{ .Age | print }}
{{ else }}
未成年人
{{ end }}
`

// 輸出
//個人資訊:
//姓名: Jackson       
//年齡: 20            
//愛好: 唱,跳,rap,籃球
//成年人              
//20 

迴圈控制range

template同時也提供了迴圈控制的功能。我們還是先看一下doc

{range pipeline}} T1 {{end}} pipeline 的值必須是陣列、切片、對映或通道。
    如果管道的值長度為零,則不輸出任何內容;
    否則,將點設定為陣列的連續元素,
    切片或對映並執行 T1。如果值是對映並且鍵是具有定義順序的基本型別,則將按排序鍵順序訪問
    
{{range pipeline}} T1 {{else}} T0 {{end}} 
    pipeline 的值必須是陣列、切片、對映或通道。
    如果管道的值長度為零,則 . 不受影響並
    執行 T0;否則,將 . 設定為陣列、切片或對映的連續元素,並執行 T1。
    
{{break}}
    最裡面的 {{range pipeline}} 迴圈提前結束,停止當前迭代並繞過所有剩餘迭代。
    
{{continue}}
    最裡面的 {{range pipeline}} 迴圈的跳過當前迭代

整合上面的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 . }}遍歷傳入的物件,在迴圈內部再透過{{ 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.函式

在第二節內容中,我們使用了print,gt函式,這些函式都是預定義在template中。我們透過查閱原始碼可以檢視預定義了以下函式:

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, // !=
    }
}

在實際開發中,僅僅是這些函式是很難滿足我們的需求。此時,我們希望能夠傳入自定義函式,在我們編寫模板的時候可以使用自定義的函式。

我們引入一個需求: 希望將傳入的str可以轉為小寫。

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支援鏈式呼叫,所以我們一般把Parse放在最後

我們透過呼叫Funcs,傳入functionName : function的map。

執行模板時,函式從兩個函式map中查詢:首先是模板函式map,然後是全域性函式map。一般不在模板內定義函式,而是使用Funcs方法新增函式到模板裡。

方法必須有一到兩個返回值,如果是兩個,那麼第二個一定是error介面型別

注意:Funcs必須在解析parse前呼叫。如果模板已經解析了,再傳入funcs,template並不知道該函式應該如何對映。

4.變數

函式、管道符、物件和控制結構都可以控制,我們轉向很多程式語言中更基本的思想之一:變數。 在模板中,很少被使用。但是我們可以使用變數簡化程式碼,並更好地使用withrange

我們透過{{ $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,那麼在內部作用域可以直接透過獲取該變數

我們透過{{- range $k,$v := . }}遍歷map中每一個KV,這種寫法類似於Golang的for-range

5.命名模板

在Go語言的模板引擎中,命名模板是指透過給模板賦予一個唯一的名稱,將其儲存在模板集中,以便後續可以透過該名稱來引用和執行該模板。

透過使用命名模板,你可以將一組相關的模板邏輯組織在一起,並在需要的時候方便地呼叫和重用它們。這對於構建複雜的模板結構和提高模板的可維護性非常有用。

在編寫複雜模板的時候,我們總是希望可以抽象出公用模板,那麼此時就需要使用命名模板進行復用。

本節將基於K8sPod模板的案例來學習如何使用命名模板進行抽象複用。

我們看一下doc

{{template "name"}}
    具有指定名稱的模板以無資料執行。

{{template "name" pipeline}}
    具有指定名稱的模板以pipeline結果執行。

透過define定義模板名稱

{{ define "container" }}
    模板
{{ end }}

透過template使用模板

{{ template "container" }}
我們在使用template.New傳入的name,實際上就是定義了模板的名稱

案例:我們希望抽象出Pod的container,透過程式碼來傳入資料生成container,避免重複的編寫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" .}}中可以傳入物件資料。

在實際開發中,我們往往不會採用列印的方式輸出。可以根據不同的需求,在Execute執行時選擇不同的io.Writer。往往我們更希望寫入到檔案中。

6.Template常用函式

func Must(t *Template, err error) *Template

Must是一個helper函式,它封裝對返回(Template, error)的函式的呼叫,並在錯誤非nil時panic。它旨在用於template初始化。

// 解析指定檔案
// 示例: ParseFiles(./pod.tpl) 
func ParseFiles(filenames ...string) (*Template, error)


// 解析filepath.Match匹配檔案
// 示例: ParseGlob(/data/*.tpl)
func ParseGlob(pattern string) (*Template, error)

這兩個函式幫助我們解析檔案中的模板,大多數情況下我們都是將模板寫在.tpl結尾的檔案中。透過不同的解析規則解析對應的檔案。

func (t *Template) Templates() []*Template 

返回當前t相關的模板的slice,包括t本身。

func (t *Template) ExecuteTemplate(wr io.Writer, name string, data any) error

傳入模板名稱,執行指定的模板。

如果在執行模板或寫入其輸出時發生錯誤,執行將停止,但部分結果可能已經被寫入輸出寫入器。模板可以安全地並行執行,但如果並行執行共享一個Writer,則輸出可能交錯。
func (t *Template) Delims(left, right string) *Template

修改模板中的分界符,可以將{{}}修改為<>

func (t *Template) Clone() (*Template, error) 

clone返回模板的副本,包括所有關聯模板。在clone的副本上新增模板是不會影響原始模板的。所以我們可以將其用於公共模板,透過clone獲取不同的副本。

7.總結

Golang的template提高程式碼重用性:模板引擎允許你建立可重用的模板片段。透過將重複的模板邏輯提取到單獨的模板中,並在需要時進行呼叫,可以減少程式碼重複,提高程式碼的可維護性和可擴充套件性。有許多code-gen使用了template + cobra方式生成複用程式碼和模板程式碼,有利於我們解放雙手。

一起進步

原文連結:https://mp.weixin.qq.com/s/SXQt6aPTj3VOdvu0P39mXg

​獨行難,眾行易,一個人刻意練習是​孤獨的。

歡迎加入我們的小圈子,一起刻意練習,結伴成長!

微訊號:wangzhongyang1993

公眾號:程式設計師升職加薪之旅

也歡迎大家關注我的賬號,點贊、留言、轉發。你的支援,是我更文的最大動力!

相關文章