Go標準庫:深入剖析Go template

来源:https://www.cnblogs.com/f-ck-need-u/archive/2018/11/28/10035768.html
-Advertisement-
Play Games

本文只關註Go text/template的底層結構,帶上了很詳細的圖片以及示例幫助理解,有些地方也附帶上了源碼進行解釋。有了本文的解釋,對於Go template的語法以及html/template的用法,一切都很簡單。 入門示例 上面定義了一個Person結構,有兩個 大寫字母開頭 (意味著這倆 ...


本文只關註Go text/template的底層結構,帶上了很詳細的圖片以及示例幫助理解,有些地方也附帶上了源碼進行解釋。有了本文的解釋,對於Go template的語法以及html/template的用法,一切都很簡單。

入門示例

package main

import (
    "html/template"
    "os"
)

type Person struct {
    Name string
    Age    int
}

func main() {
    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類型。

上圖中使用紅色框線框起來一部分返回值是*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。

大概結構如下圖:

除了需要關註的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為模板名稱。

因為還沒有進行解析操作,所以上圖使用虛線表示尚不存在的部分。

實際上,在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 0xc0420a6000 0xc0420640c0  }
&{test1 0xc0420a6000 0xc0420640c0  }
&{test1 0xc0420a6000 0xc0420640c0  }
&{T1 0xc0420a6100 0xc0420640c0  }
&{T2 0xc0420a6200 0xc0420640c0  }
&{T3 0xc0420a6300 0xc0420640c0  }

首先使用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個元素。

結構如下圖:

必須註意,雖然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")

結構圖:

如果t1和t2的Parse()中,都定義一個或多個name相同的模板會如何?例如:

t1 := template.New("test1")
t2 := t1.New("test2")
t1, _ = t1.Parse(
    `{{define "T1"}}ONE{{end}}
    {{define "T2"}}TWO{{end}}
    {{define "T3"}}{{template "T1"}} {{template "T2"}}{{end}}
    {{template "T3"}}`)
t2, _ = t2.Parse(
    `{{define "T4"}}ONE{{end}}
    {{define "T2"}}TWOO{{end}}
    {{define "T3"}}{{template "T4"}} {{template "T2"}}{{end}}
    {{template "T3"}}`)

    _ = t1.Execute(os.Stdout, "a")
    _ = t2.Execute(os.Stdout, "a")

在上面的t1和t2中,它們共用同一個common,且t1.Parse()中定義了T1、T2和T3,t2.Parse()中定義了T4、T2和T3,且兩個T2的解析內容不一樣(解析樹不一樣)。

因為T1、T2、T3、T4都會加入到t1和t2共用的common中,所以無論是通過t1還是通過t2這兩個關聯名稱都能找到T1、T2、T3、T4。但是後解析的會覆蓋先解析的,也就是說,無論是t1.Lookup("T2")還是t2.Lookup("T2")得到的T2對應的template,都是在t2.Parse()中定義的。當t1.Execute()的時候,會得到t2中定義的T2的值。

ONE TWOO
ONE TWOO

Parse()

Parse(string)方法用於解析給定的文本內容string。用法上很簡單,前面也已經用過幾次了,沒什麼可解釋的。重點在於它的作用。

當創建了一個模板對象後,會有一個與之關聯的common(如果不存在,template包中的各種函數、方法都會因為調用init()方法而保證common的存在)。只有在Parse()之後,才會將相關的template name放進common中,表示這個模板已經可用了,或者稱為已經定義了(defined),可用被Execute()或ExecuteTemplate(),也表示可用使用Lookup()和DefinedTemplates()來檢索模板。另外,調用了Parse()解析後,會將給定的FuncMap中的函數添加到common的FuncMap中,只有添加到common的函數,才可以在模板中使用。

Parse()方法是解析字元串的,且只解析New()出來的模板對象。如果想要解析文件中的內容,見後文ParseFiles()、ParseGlob()。

Lookup()、DefinedTemplates()和Templates()方法

這三個方法都用於檢索已經定義的模板,Lookup()根據template name來檢索並返回對應的template,DefinedTemplates()則是返回所有已定義的templates。Templates()和DefinedTemplates()類似,但是它返回的是[]*Template,也就是已定義的template的slice。

前面多次說過,只有在解析之後,模板才加入到common結構中,才算是已經定義,才能被檢索或執行。

當檢索不存在的templates時,Lookup()將返回nil。當common中沒有模板,DefinedTemplates()將返回空字元串"",Templates()將返回空的slice。

func main() {
    t1 := template.New("test1")
    t2 := t1.New("test2")
    t1, _ = t1.Parse(
        `{{define "T1"}}ONE{{end}}
        {{define "T2"}}TWO{{end}}
        {{define "T3"}}{{template "T1"}} {{template "T2"}}{{end}}
        {{template "T3"}}`)
    t2, _ = t2.Parse(
        `{{define "T4"}}ONE{{end}}
        {{define "T2"}}TWOO{{end}}
        {{define "T3"}}{{template "T4"}} {{template "T2"}}{{end}}
        {{template "T3"}}`)

    fmt.Println(t1.DefinedTemplates())
    fmt.Println(t2.DefinedTemplates())
    fmt.Println(t2.Templates())
}

返回結果:

; defined templates are: "T1", "T2", "T3", "test1", "T4", "test2"
; defined templates are: "test1", "T4", "test2", "T1", "T2", "T3"
[0xc04201c280 0xc042064100 0xc04201c1c0 0xc04201c2c0 0xc04201c300 0xc042064080]

從結果可見,返回的順序雖然不一致,但包含的template name是完全一致的。

Clone()方法

Clone()方法用於克隆一個完全一樣的模板,包括common結構也會完全克隆

t1 := template.New("test1")
t1 = t1.Parse(...)
t2 := t1.New("test2")
t2 = t2.Parse(...)

t3, err := t1.Clone()
if err != nil {
    panic(err)
}

這裡的t3和t1在內容上完全一致,但在記憶體中它們是兩個不同的對象。但無論如何,目前t3中會包含t1和t2共用的common,即使t2中定義了{{define "Tx"}}...{{end}},這個Tx也會包含在t3中。

因為是不同的對象,所以修改t3,不會影響t1/t2。

看下麵的例子:

func main() {
    t1 := template.New("test1")
    t2 := t1.New("test2")
    t1, _ = t1.Parse(
        `{{define "T1"}}ONE{{end}}
        {{define "T2"}}TWO{{end}}
        {{define "T3"}}{{template "T1"}} {{template "T2"}}{{end}}
        {{template "T3"}}`)
    t2, _ = t2.Parse(
        `{{define "T4"}}ONE{{end}}
        {{define "T2"}}TWOO{{end}}
        {{define "T3"}}{{template "T4"}} {{template "T2"}}{{end}}
        {{template "T3"}}`)

    t3, err := t1.Clone()
    if err != nil {
        panic(err)
    }

    // 結果完全一致
    fmt.Println(t1.Lookup("T4"))
    fmt.Println(t3.Lookup("T4"))
    
    // 修改t3
    t3,_ = t3.Parse(`{{define "T4"}}one{{end}}`)
    // 結果將不一致
    fmt.Println(t1.Lookup("T4"))
    fmt.Println(t3.Lookup("T4"))
}

Must()函數

正常情況下,很多函數、方法都返回兩個值,一個是想要返回的值,一個是err信息。template包中的函數、方法也一樣如此。

但有時候不想要err信息,而是直接取第一個返回值,並賦值給變數。操作大概是這樣的:

t1 := template.New("ttt")
t1,err := t1.Parse(...)
if err != nil {
    panic(err)
}
...

Must()函數將上面的過程封裝了,使得Must()可以簡化上面的操作:

func Must(t *Template, err error) *Template {
    if err != nil {
        panic(err)
    }
    return t
}

當某個返回*Template,err的函數、方法需要直接使用時,可用將其包裝在Must()中,它會自動在有err的時候panic,無錯的時候只返回其中的*Template

這在賦值給變數的時候非常簡便,例如:

var t = template.Must(template.New("name").Parse("text"))

ParseFiles()和ParseGlob()

Parse()只能解析字元串,要解析文件中的內容,需要使用ParseFiles()或ParseGlob()。

template包中有ParseFiles()和ParseGlob()函數,也有ParseFiles()和ParseGlob()方法。

這兩個函數和這兩個方法的區別,看一下文檔就很清晰:

$ go doc template.ParseFiles
func ParseFiles(filenames ...string) (*Template, error)
    ParseFiles creates a new Template and parses the template definitions from
    the named files. The returned template's name will have the (base) name and
    (parsed) contents of the first file. There must be at least one file. If an
    error occurs, parsing stops and the returned *Template is nil.

$ go doc template.template.ParseFiles
func (t *Template) ParseFiles(filenames ...string) (*Template, error)
    ParseFiles parses the named files and associates the resulting templates
    with t. If an error occurs, parsing stops and the returned template is nil;
    otherwise it is t. There must be at least one file.

解釋很清晰。ParseFiles()函數是直接解析一個或多個文件的內容,並返回第一個文件名的basename作為Template的名稱,也就是說這些文件的template全都關聯到第一個文件的basename上。ParseFiles()方法則是解析一個或多個文件的內容,並將這些內容關聯到t上。

看示例就一目瞭然。

例如,當前go程式的目錄下有3個文件:a.cnf、b.cnf和c.cnf,它們的內容無所謂,反正空內容也可以解析。

func main() {
    t1,err := template.ParseFiles("a.cnf","b.cnf","c.cnf")
    if err != nil {
        panic(err)
    }
    fmt.Println(t1.DefinedTemplates())
    fmt.Println()
    fmt.Println(t1)
    fmt.Println(t1.Lookup("a.cnf"))
    fmt.Println(t1.Lookup("b.cnf"))
    fmt.Println(t1.Lookup("c.cnf"))
}

輸出結果:

; defined templates are: "a.cnf", "b.cnf", "c.cnf"

&{a.cnf 0xc0420ae000 0xc042064140  }
&{a.cnf 0xc0420ae000 0xc042064140  }
&{b.cnf 0xc0420bc000 0xc042064140  }
&{c.cnf 0xc0420bc100 0xc042064140  }

從結果中可以看到,已定義的template name都是文件的basename,且t1和a.cnf這個template是完全一致的,即t1是文件列表中的第一個模板對象。

結構如下圖:

理解了ParseFiles()函數,理解ParseFiles()方法、ParseGlob()函數、ParseGlob()方法,應該不會再有什麼問題。但是還是有需要註意的地方:

func main() {
    t1 := template.New("test")
    t1,err := t1.ParseFiles("a.cnf","b.cnf","c.cnf")
    if err != nil {
        panic(err)
    }
    // 先註釋下麵這行
    //t1.Parse("")
    fmt.Println(t1.DefinedTemplates())
    fmt.Println()
    fmt.Println(t1)
    fmt.Println(t1.Lookup("a.cnf"))
    fmt.Println(t1.Lookup("b.cnf"))
    fmt.Println(t1.Lookup("c.cnf"))
}

執行結果:

; defined templates are: "a.cnf", "b.cnf", "c.cnf"

&{test <nil> 0xc0420640c0  }
&{a.cnf 0xc0420b0000 0xc0420640c0  }
&{b.cnf 0xc0420be000 0xc0420640c0  }
&{c.cnf 0xc0420be100 0xc0420640c0  }

發現template.New()函數創建的模板對象test並沒有包含到common中。為什麼?

因為t.ParseFiles()、t.ParseGlob()方法的解析過程是獨立於t之外的,它們只解析文件內容,不解析字元串。而New()出來的模板,需要Parse()方法來解析才會加入到common中。

將上面的註釋行取消掉,執行結果將如下:

; defined templates are: "a.cnf", "b.cnf", "c.cnf", "test"

&{test 0xc0420bc200 0xc0420640c0  }
&{a.cnf 0xc0420ae000 0xc0420640c0  }
&{b.cnf 0xc0420bc000 0xc0420640c0  }
&{c.cnf 0xc0420bc100 0xc0420640c0  }

具體原因可分析parseFiles()源碼:

func parseFiles(t *Template, filenames ...string) (*Template, error) {
    if len(filenames) == 0 {
        // Not really a problem, but be consistent.
        return nil, fmt.Errorf("template: no files named in call to ParseFiles")
    }
    for _, filename := range filenames {
        b, err := ioutil.ReadFile(filename)
        if err != nil {
            return nil, err
        }
        s := string(b)

        // name為文件名的basename部分
        name := filepath.Base(filename)

        var tmpl *Template
        if t == nil {
            t = New(name)
        }
        // 如果調用t.Parsefiles(),則t.Name不為空
        // name也就不等於t.Name
        // 於是新New(name)一個模板對象給tmpl
        if name == t.Name() {
            tmpl = t
        } else {
            tmpl = t.New(name)
        }
        // 解析tmpl。如果選中了上面的else分支,則和t無關
        _, err = tmpl.Parse(s)
        if err != nil {
            return nil, err
        }
    }
    return t, nil
}

Execute()和ExecuteTemplate()

這兩個方法都可以用來應用已經解析好的模板,應用表示對需要評估的數據進行操作,並和無需評估數據進行合併,然後輸出到io.Writer中:

func (t *Template) Execute(wr io.Writer, data interface{}) error
func (t *Template) ExecuteTemplate(wr io.Writer, name string, data interface{}) error

兩者的區別在於Execute()是應用整個common中已定義的模板對象,而ExecuteTemplate()可以選擇common中某個已定義的模板進行應用。

例如:

func main() {
    t1 := template.New("test1")
    t1, _ = t1.Parse(`{{define "T1"}}ONE{{end}}
        {{- define "T2"}}TWO{{end}}
        {{- define "T3"}}{{template "T1"}} {{template "T2"}}{{end}}
        {{- template "T3"}}`)
    
    _ = t1.Execute(os.Stdout,"")
    fmt.Println()
    fmt.Println("-------------")
    _ = t1.ExecuteTemplate(os.Stdout, "T2", "")
}

輸出結果:

ONE TWO
-------------
TWO

FuncMap和Funcs()

template內置了一系列函數,但這些函數畢竟有限,可能無法滿足特殊的需求。template允許我們定義自己的函數,添加到common中,然後就可以在待解析的內容中像使用內置函數一樣使用自定義的函數。

自定義函數的優先順序高於內置的函數優先順序,即先檢索自定義函數,再檢索內置函數。也就是說,如果自定義函數的函數名和內置函數名相同,則內置函數將失效。

本文只對此稍作解釋,本文的重點不是template的具體語法和用法。

在common結構中,有一個欄位是FuncMap類型的:

type common struct {
    tmpl   map[string]*Template
    option option
    muFuncs    sync.RWMutex // protects parseFuncs and execFuncs
    parseFuncs FuncMap
    execFuncs  map[string]reflect.Value
}

這個類型的定義為:

type FuncMap map[string]interface{}

它是一個map結構,key為模板中可以使用的函數名,value為函數對象(為了方便稱呼,這裡直接成為函數)。函數必須只有1個值或2個值,如果有兩個值,第二個值必須是error類型的,當執行函數時err不為空,則執行自動停止。

函數可以有多個參數。假如函數str有兩個參數,在待解析的內容中調用函數str時,如果調用方式為{{str . "aaa"}},表示第一個參數為當前對象,第二個參數為字元串"aaa"。

假如,要定義一個將字元串轉換為大寫的函數,可以:

import "strings"
func upper(str string) string {
    return strings.ToUpper(str)
}

然後將其添加到FuncMap結構中,並將此函數命名為"strupper",以後在待解析的內容中就可以調用"strupper"函數。

funcMap := template.FuncMap{
    "strupper": upper,
}

或者,直接將匿名函數放在FuncMap內部:

funcMap := template.FuncMap{
    "strupper": func(str string) string { return strings.ToUpper(str) },
}

現在只是定義了一個FuncMap實例,這個實例中有一個函數。還沒有將它關聯到模板,嚴格地說還沒有將其放進common結構。要將其放進common結構,調用Funcs()方法(其實調用此方法也沒有將其放進common,只有在解析的時候才會放進common):

func (t *Template) Funcs(funcMap FuncMap) *Template

例如:

funcMap := template.FuncMap{
    "strupper": func(str string) string { return strings.ToUpper(str) },
}
t1 := template.New("test")
t1 = t1.Funcs(funcMap)

這樣,和t1共用common的所有模板都可以調用"strupper"函數。

註意,必須在解析之前調用Funcs()方法,在解析的時候會將函數放進common結構。

下麵是完整的示例代碼:

package main

import (
    "os"
    "strings"
    "text/template"
)

func main() {
    funcMap := template.FuncMap{
        "strupper": upper,
    }
    t1 := template.New("test1")
    tmpl, err := t1.Funcs(funcMap).Parse(`{{strupper .}}`)
    if err != nil {
        panic(err)
    }
    _ = tmpl.Execute(os.Stdout, "go programming")
}

func upper(str string) string {
    return strings.ToUpper(str)
}

上面調用了{{strupper .}},這裡的strupper是我們自定義的函數,"."是它的參數(註意,參數不是放進括弧里)。這裡的"."代表當前作用域內的當前對象,對於這個示例來說,當前對象就是那段字元串對象"go programming"。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 單例模式 介紹 模式:創建型 意圖:保證一個類只有一個實例,並提供一個訪問它的全局訪問點 解決:一個全局使用的類頻繁地創建與銷毀 場景: 唯一序列號 web中的計數器 I/O與資料庫的連接 …… 唯一序列號 web中的計數器 I/O與資料庫的連接 …… 實現方式 餓漢式 :靜態載入,線程安全 餓漢式 ...
  • 1. 簡單工廠 1. 你開了一家披薩店,點披薩的方法可能是這樣: 可以看到,每當你想增加一種披薩類型,就要修改代碼,添加一種if else條件.當有多個系統存在orderPizza的需求時,每個系統都要同時修改他們的代碼.因此,需要將這種實例化具體對象的代碼封裝起來. 這就是簡單工廠方法,他不算一種 ...
  • 迭代器模式(Iterator Pattern)是最常被使用的幾個模式之一,被廣泛地應用到Java的API中。 定義:提供一種方法訪問一個容器對象中各個元素,而又不需暴露該對象的內部細節。 類圖如下所示。 迭代器模式有以下4個角色。 抽象迭代器(Iterator)角色:負責定義訪問和遍歷元素的介面。 ...
  • 參考於 : 大話設計模式 馬士兵設計模式視頻 代碼參考於馬士兵設計模式視頻 寫在開頭:職責鏈模式:使多個對象都有機會處理請求,從而避免請求的發送者和接收者之間的耦合關係 圖來自大話設計模式,下麵我的代碼中,Clien是t依賴於Handler1和Handler2的,不過可以使用配置文件或者直接給Fil ...
  • 搭建spark本地環境 搭建Java環境 (1)到官網下載JDK 官網鏈接:https://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html (2)解壓縮到指定的目錄 (3)設置路徑和環境變數 在 ...
  • 一.結構型設計模式 門面模式與單例模式,工廠模式不同,它是一種結構型模式。 結構型模式描述如何將對象和類組合成更大的結構 結構型模式是一種能夠簡化設計工作的模式,它能找出更簡單的方法來認識或表示實體之間的關係。 結構型模式是類和對象模式的綜合體。類模式通過繼承來描述抽象,從而提供更有用的程式介面,而 ...
  • 最近工作中慢慢開始用python協程相關的東西,所以用到了一些相關模塊,如aiohttp, aiomysql, aioredis等,用的過程中也碰到的很多問題,這裡整理了一次記憶體泄漏的問題 通常我們寫python程式的時候也很少關註記憶體這個問題(當然可能我的能力還有待提升),可能寫c和c++的朋友會 ...
  • 今天這篇博文來探索一下laravel的路由。在第一篇講laravel入口文件的博文里,我們就提到過laravel的路由是在application對象的初始化階段,通過provider來載入的。這個路由服務提供者註冊於vendor\laravel\framework\src\Illuminate\Fo ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...