1. 程式人生 > >推薦系統演算法工程師-從入門到就業

推薦系統演算法工程師-從入門到就業

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

入門示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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

例如:

1
2
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結構:

1
2
3
4
5
6
7
type Template struct {
    name string
    *parse.Tree
    *common
    leftDelim  string
    rightDelim string
}

name是這個Template的名稱,Tree是解析樹,common是另一個結構,稍後解釋。leftDelim和rightDelim是左右兩邊的分隔符,預設為{{}}

這裡主要關注name和common兩個欄位,name欄位沒什麼解釋的。common是一個結構:

1
2
3
4
5
6
7
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,也就是模板組

1
2
3
4
5
6
7
func New(name string) *Template {
    t := &Template{
        name: name,
    }
    t.init()
    return t
}

其中t為模板的關聯名稱,name為模板的名稱,t.init()表示如果模板物件t還沒有common結構,就構造一個新的common組:

1
2
3
4
5
6
7
8
9
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。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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行的結果完全一致,所有行的第二個地址完全相同。

1
2
3
4
5
6
&{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個元素。