fasttemplate
是一個比較簡單、易用的小型模板庫。fasttemplate
的做者valyala另外還開源了很多優秀的庫,如大名鼎鼎的fasthttp
,前面介紹的bytebufferpool
,還有一個重量級的模板庫quicktemplate
。quicktemplate
比標準庫中的text/template
和html/template
要靈活和易用不少,後面會專門介紹它。今天要介紹的fasttemlate
只專一於一塊很小的領域——字符串替換。它的目標是爲了替代strings.Replace
、fmt.Sprintf
等方法,提供一個簡單,易用,高性能的字符串替換方法。html
本文首先介紹fasttemplate
的用法,而後去看看源碼實現的一些細節。git
本文代碼使用 Go Modules。github
建立目錄並初始化:golang
$ mkdir fasttemplate && cd fasttemplate $ go mod init github.com/darjun/go-daily-lib/fasttemplate
安裝fasttemplate
庫:微信
$ go get -u github.com/valyala/fasttemplate
編寫代碼:app
package main import ( "fmt" "github.com/valyala/fasttemplate" ) func main() { template := `name: {{name}} age: {{age}}` t := fasttemplate.New(template, "{{", "}}") s1 := t.ExecuteString(map[string]interface{}{ "name": "dj", "age": "18", }) s2 := t.ExecuteString(map[string]interface{}{ "name": "hjw", "age": "20", }) fmt.Println(s1) fmt.Println(s2) }
{{
和}}
表示佔位符,佔位符能夠在建立模板的時候指定;fasttemplate.New()
建立一個模板對象t
,傳入開始和結束佔位符;t.ExecuteString()
方法,傳入參數。參數中有各個佔位符對應的值。生成最終的字符串。運行結果:函數
name: dj age: 18
咱們能夠自定義佔位符,上面分別使用{{
和}}
做爲開始和結束佔位符。咱們能夠換成[[
和]]
,只須要簡單修改一下代碼便可:源碼分析
template := `name: [[name]] age: [[age]]` t := fasttemplate.New(template, "[[", "]]")
另外,須要注意的是,傳入參數的類型爲map[string]interface{}
,可是fasttemplate
只接受類型爲[]byte
、string
和TagFunc
類型的值。這也是爲何上面的18
要用雙引號括起來的緣由。性能
另外一個須要注意的點,fasttemplate.New()
返回一個模板對象,若是模板解析失敗了,就會直接panic
。若是想要本身處理錯誤,能夠調用fasttemplate.NewTemplate()
方法,該方法返回一個模板對象和一個錯誤。實際上,fasttemplate.New()
內部就是調用fasttemplate.NewTemplate()
,若是返回了錯誤,就panic
:學習
// src/github.com/valyala/fasttemplate/template.go func New(template, startTag, endTag string) *Template { t, err := NewTemplate(template, startTag, endTag) if err != nil { panic(err) } return t } func NewTemplate(template, startTag, endTag string) (*Template, error) { var t Template err := t.Reset(template, startTag, endTag) if err != nil { return nil, err } return &t, nil }
這其實也是一種慣用法,對於不想處理錯誤的示例程序,直接panic
有時也是一種選擇。例如html.template
標準庫也提供了Must()
方法,通常這樣用,遇到解析失敗就panic
:
t := template.Must(template.New("name").Parse("html"))
佔位符中間內部不要加空格!!!
佔位符中間內部不要加空格!!!
佔位符中間內部不要加空格!!!
使用fasttemplate.New()
定義模板對象的方式,咱們能夠屢次使用不一樣的參數去作替換。可是,有時候咱們要作大量一次性的替換,每次都定義模板對象顯得比較繁瑣。fasttemplate
也提供了一次性替換的方法:
func main() { template := `name: [name] age: [age]` s := fasttemplate.ExecuteString(template, "[", "]", map[string]interface{}{ "name": "dj", "age": "18", }) fmt.Println(s) }
使用這種方式,咱們須要同時傳入模板字符串、開始佔位符、結束佔位符和替換參數。
TagFunc
fasttemplate
提供了一個TagFunc
,能夠給替換增長一些邏輯。TagFunc
是一個函數:
type TagFunc func(w io.Writer, tag string) (int, error)
在執行替換的時候,fasttemplate
針對每一個佔位符都會調用一次TagFunc
函數,tag
即佔位符的名稱。看下面程序:
func main() { template := `name: {{name}} age: {{age}}` t := fasttemplate.New(template, "{{", "}}") s := t.ExecuteFuncString(func(w io.Writer, tag string) (int, error) { switch tag { case "name": return w.Write([]byte("dj")) case "age": return w.Write([]byte("18")) default: return 0, nil } }) fmt.Println(s) }
這其實就是get-started
示例程序的TagFunc
版本,根據傳入的tag
寫入不一樣的值。若是咱們去查看源碼就會發現,實際上ExecuteString()
最終仍是會調用ExecuteFuncString()
。fasttemplate
提供了一個標準的TagFunc
:
func (t *Template) ExecuteString(m map[string]interface{}) string { return t.ExecuteFuncString(func(w io.Writer, tag string) (int, error) { return stdTagFunc(w, tag, m) }) } func stdTagFunc(w io.Writer, tag string, m map[string]interface{}) (int, error) { v := m[tag] if v == nil { return 0, nil } switch value := v.(type) { case []byte: return w.Write(value) case string: return w.Write([]byte(value)) case TagFunc: return value(w, tag) default: panic(fmt.Sprintf("tag=%q contains unexpected value type=%#v. Expected []byte, string or TagFunc", tag, v)) } }
標準的TagFunc
實現也很是簡單,就是從參數map[string]interface{}
中取出對應的值作相應處理,若是是[]byte
和string
類型,直接調用io.Writer
的寫入方法。若是是TagFunc
類型則直接調用該方法,將io.Writer
和tag
傳入。其餘類型直接panic
拋出錯誤。
若是模板中的tag
在參數map[string]interface{}
中不存在,有兩種處理方式:
""
。標準的stdTagFunc
就是這樣處理的;tag
。keepUnknownTagFunc
就是作這個事情的。keepUnknownTagFunc
代碼以下:
func keepUnknownTagFunc(w io.Writer, startTag, endTag, tag string, m map[string]interface{}) (int, error) { v, ok := m[tag] if !ok { if _, err := w.Write(unsafeString2Bytes(startTag)); err != nil { return 0, err } if _, err := w.Write(unsafeString2Bytes(tag)); err != nil { return 0, err } if _, err := w.Write(unsafeString2Bytes(endTag)); err != nil { return 0, err } return len(startTag) + len(tag) + len(endTag), nil } if v == nil { return 0, nil } switch value := v.(type) { case []byte: return w.Write(value) case string: return w.Write([]byte(value)) case TagFunc: return value(w, tag) default: panic(fmt.Sprintf("tag=%q contains unexpected value type=%#v. Expected []byte, string or TagFunc", tag, v)) } }
後半段處理與stdTagFunc
同樣,函數前半部分若是tag
未找到。直接寫入startTag
+ tag
+ endTag
做爲替換的值。
咱們前面調用的ExecuteString()
方法使用stdTagFunc
,即直接將未識別的tag
替換成空字符串。若是想保留未識別的tag
,改成調用ExecuteStringStd()
方法便可。該方法遇到未識別的tag
會保留:
func main() { template := `name: {{name}} age: {{age}}` t := fasttemplate.New(template, "{{", "}}") m := map[string]interface{}{"name": "dj"} s1 := t.ExecuteString(m) fmt.Println(s1) s2 := t.ExecuteStringStd(m) fmt.Println(s2) }
參數中缺乏age
,運行結果:
name: dj age: name: dj age: {{age}}
io.Writer
參數的方法前面介紹的方法最後都是返回一個字符串。方法名中都有String
:ExecuteString()/ExecuteFuncString()
。
咱們能夠直接傳入一個io.Writer
參數,將結果字符串調用這個參數的Write()
方法直接寫入。這類方法名中沒有String
:Execute()/ExecuteFunc()
:
func main() { template := `name: {{name}} age: {{age}}` t := fasttemplate.New(template, "{{", "}}") t.Execute(os.Stdout, map[string]interface{}{ "name": "dj", "age": "18", }) fmt.Println() t.ExecuteFunc(os.Stdout, func(w io.Writer, tag string) (int, error) { switch tag { case "name": return w.Write([]byte("hjw")) case "age": return w.Write([]byte("20")) } return 0, nil }) }
因爲os.Stdout
實現了io.Writer
接口,能夠直接傳入。結果直接寫到os.Stdout
中。運行:
name: dj age: 18 name: hjw age: 20
首先看模板對象的結構和建立:
// src/github.com/valyala/fasttemplate/template.go type Template struct { template string startTag string endTag string texts [][]byte tags []string byteBufferPool bytebufferpool.Pool } func NewTemplate(template, startTag, endTag string) (*Template, error) { var t Template err := t.Reset(template, startTag, endTag) if err != nil { return nil, err } return &t, nil }
模板建立以後會調用Reset()
方法初始化:
func (t *Template) Reset(template, startTag, endTag string) error { t.template = template t.startTag = startTag t.endTag = endTag t.texts = t.texts[:0] t.tags = t.tags[:0] if len(startTag) == 0 { panic("startTag cannot be empty") } if len(endTag) == 0 { panic("endTag cannot be empty") } s := unsafeString2Bytes(template) a := unsafeString2Bytes(startTag) b := unsafeString2Bytes(endTag) tagsCount := bytes.Count(s, a) if tagsCount == 0 { return nil } if tagsCount+1 > cap(t.texts) { t.texts = make([][]byte, 0, tagsCount+1) } if tagsCount > cap(t.tags) { t.tags = make([]string, 0, tagsCount) } for { n := bytes.Index(s, a) if n < 0 { t.texts = append(t.texts, s) break } t.texts = append(t.texts, s[:n]) s = s[n+len(a):] n = bytes.Index(s, b) if n < 0 { return fmt.Errorf("Cannot find end tag=%q in the template=%q starting from %q", endTag, template, s) } t.tags = append(t.tags, unsafeBytes2String(s[:n])) s = s[n+len(b):] } return nil }
初始化作了下面這些事情:
tag
切分開,分別存放在texts
和tags
切片中。後半段的for
循環就是作的這個事情。代碼細節點:
tag
切片,注意構造正確的模板字符串文本切片必定比tag
切片大 1。像這樣| text | tag | text | ... | tag | text |
;unsafeString2Bytes
讓返回的字節切片直接指向string
內部地址。看上面的介紹,貌似有不少方法。實際上核心的方法就一個ExecuteFunc()
。其餘的方法都是直接或間接地調用它:
// src/github.com/valyala/fasttemplate/template.go func (t *Template) Execute(w io.Writer, m map[string]interface{}) (int64, error) { return t.ExecuteFunc(w, func(w io.Writer, tag string) (int, error) { return stdTagFunc(w, tag, m) }) } func (t *Template) ExecuteStd(w io.Writer, m map[string]interface{}) (int64, error) { return t.ExecuteFunc(w, func(w io.Writer, tag string) (int, error) { return keepUnknownTagFunc(w, t.startTag, t.endTag, tag, m) }) } func (t *Template) ExecuteFuncString(f TagFunc) string { s, err := t.ExecuteFuncStringWithErr(f) if err != nil { panic(fmt.Sprintf("unexpected error: %s", err)) } return s } func (t *Template) ExecuteFuncStringWithErr(f TagFunc) (string, error) { bb := t.byteBufferPool.Get() if _, err := t.ExecuteFunc(bb, f); err != nil { bb.Reset() t.byteBufferPool.Put(bb) return "", err } s := string(bb.Bytes()) bb.Reset() t.byteBufferPool.Put(bb) return s, nil } func (t *Template) ExecuteString(m map[string]interface{}) string { return t.ExecuteFuncString(func(w io.Writer, tag string) (int, error) { return stdTagFunc(w, tag, m) }) } func (t *Template) ExecuteStringStd(m map[string]interface{}) string { return t.ExecuteFuncString(func(w io.Writer, tag string) (int, error) { return keepUnknownTagFunc(w, t.startTag, t.endTag, tag, m) }) }
Execute()
方法構造一個TagFunc
調用ExecuteFunc()
,內部使用stdTagFunc
:
func(w io.Writer, tag string) (int, error) { return stdTagFunc(w, tag, m) }
ExecuteStd()
方法構造一個TagFunc
調用ExecuteFunc()
,內部使用keepUnknownTagFunc
:
func(w io.Writer, tag string) (int, error) { return keepUnknownTagFunc(w, t.startTag, t.endTag, tag, m) }
ExecuteString()
和ExecuteStringStd()
方法調用ExecuteFuncString()
方法,而ExecuteFuncString()
方法又調用了ExecuteFuncStringWithErr()
方法,ExecuteFuncStringWithErr()
方法內部使用bytebufferpool.Get()
得到一個bytebufferpoo.Buffer
對象去調用ExecuteFunc()
方法。因此核心就是ExecuteFunc()
方法:
func (t *Template) ExecuteFunc(w io.Writer, f TagFunc) (int64, error) { var nn int64 n := len(t.texts) - 1 if n == -1 { ni, err := w.Write(unsafeString2Bytes(t.template)) return int64(ni), err } for i := 0; i < n; i++ { ni, err := w.Write(t.texts[i]) nn += int64(ni) if err != nil { return nn, err } ni, err = f(w, t.tags[i]) nn += int64(ni) if err != nil { return nn, err } } ni, err := w.Write(t.texts[n]) nn += int64(ni) return nn, err }
整個邏輯也很清晰,for
循環就是Write
一個texts
元素,以當前的tag
執行TagFunc
,索引 +1。最後寫入最後一個texts
元素,完成。大概是這樣:
| text | tag | text | tag | text | ... | tag | text |
注:ExecuteFuncStringWithErr()
方法使用到了前面文章介紹的bytebufferpool
,感興趣能夠回去翻看。
可使用fasttemplate
完成strings.Replace
和fmt.Sprintf
的任務,並且fasttemplate
靈活性更高。代碼清晰易懂,值得一看。
吐槽:關於命名,Execute()
方法裏面使用stdTagFunc
,ExecuteStd()
方法裏面使用keepUnknownTagFunc
方法。我想是否是把stdTagFunc
更名爲defaultTagFunc
好一點?
你們若是發現好玩、好用的 Go 語言庫,歡迎到 Go 每日一庫 GitHub 上提交 issue😄
歡迎關注個人微信公衆號【GoUpUp】,共同窗習,一塊兒進步~