Go 每日一庫(kù)之 fasttemplate
簡(jiǎn)介
fasttemplate是一個(gè)比較簡(jiǎn)單、易用的小型模板庫(kù)。fasttemplate的作者valyala另外還開(kāi)源了不少優(yōu)秀的庫(kù),如大名鼎鼎的fasthttp,前面介紹的bytebufferpool,還有一個(gè)重量級(jí)的模板庫(kù)quicktemplate。quicktemplate比標(biāo)準(zhǔn)庫(kù)中的text/template和html/template要靈活和易用很多,后面會(huì)專(zhuān)門(mén)介紹它。今天要介紹的fasttemlate只專(zhuān)注于一塊很小的領(lǐng)域——字符串替換。它的目標(biāo)是為了替代strings.Replace、fmt.Sprintf等方法,提供一個(gè)簡(jiǎn)單,易用,高性能的字符串替換方法。
本文首先介紹fasttemplate的用法,然后去看看源碼實(shí)現(xiàn)的一些細(xì)節(jié)。
快速使用
本文代碼使用 Go Modules。
創(chuàng)建目錄并初始化:
$ mkdir fasttemplate && cd fasttemplate
$ go mod init github.com/darjun/go-daily-lib/fasttemplate
安裝fasttemplate庫(kù):
$ go get -u github.com/valyala/fasttemplate
編寫(xiě)代碼:
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)
}
定義模板字符串,使用 {{和}}表示占位符,占位符可以在創(chuàng)建模板的時(shí)候指定;調(diào)用 fasttemplate.New()創(chuàng)建一個(gè)模板對(duì)象t,傳入開(kāi)始和結(jié)束占位符;調(diào)用模板對(duì)象的 t.ExecuteString()方法,傳入?yún)?shù)。參數(shù)中有各個(gè)占位符對(duì)應(yīng)的值。生成最終的字符串。
運(yùn)行結(jié)果:
name: dj
age: 18
我們可以自定義占位符,上面分別使用{{和}}作為開(kāi)始和結(jié)束占位符。我們可以換成[[和]],只需要簡(jiǎn)單修改一下代碼即可:
template := `name: [[name]]
age: [[age]]`
t := fasttemplate.New(template, "[[", "]]")
另外,需要注意的是,傳入?yún)?shù)的類(lèi)型為map[string]interface{},但是fasttemplate只接受類(lèi)型為[]byte、string和TagFunc類(lèi)型的值。這也是為什么上面的18要用雙引號(hào)括起來(lái)的原因。
另一個(gè)需要注意的點(diǎn),fasttemplate.New()返回一個(gè)模板對(duì)象,如果模板解析失敗了,就會(huì)直接panic。如果想要自己處理錯(cuò)誤,可以調(diào)用fasttemplate.NewTemplate()方法,該方法返回一個(gè)模板對(duì)象和一個(gè)錯(cuò)誤。實(shí)際上,fasttemplate.New()內(nèi)部就是調(diào)用fasttemplate.NewTemplate(),如果返回了錯(cuò)誤,就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
}
這其實(shí)也是一種慣用法,對(duì)于不想處理錯(cuò)誤的示例程序,直接panic有時(shí)也是一種選擇。例如html.template標(biāo)準(zhǔn)庫(kù)也提供了Must()方法,一般這樣用,遇到解析失敗就panic:
t := template.Must(template.New("name").Parse("html"))
占位符中間內(nèi)部不要加空格!!!
占位符中間內(nèi)部不要加空格!!!
占位符中間內(nèi)部不要加空格!!!
快捷方式
使用fasttemplate.New()定義模板對(duì)象的方式,我們可以多次使用不同的參數(shù)去做替換。但是,有時(shí)候我們要做大量一次性的替換,每次都定義模板對(duì)象顯得比較繁瑣。fasttemplate也提供了一次性替換的方法:
func main() {
template := `name: [name]
age: [age]`
s := fasttemplate.ExecuteString(template, "[", "]", map[string]interface{}{
"name": "dj",
"age": "18",
})
fmt.Println(s)
}
使用這種方式,我們需要同時(shí)傳入模板字符串、開(kāi)始占位符、結(jié)束占位符和替換參數(shù)。
TagFunc
fasttemplate提供了一個(gè)TagFunc,可以給替換增加一些邏輯。TagFunc是一個(gè)函數(shù):
type TagFunc func(w io.Writer, tag string) (int, error)
在執(zhí)行替換的時(shí)候,fasttemplate針對(duì)每個(gè)占位符都會(huì)調(diào)用一次TagFunc函數(shù),tag即占位符的名稱(chēng)。看下面程序:
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)
}
這其實(shí)就是get-started示例程序的TagFunc版本,根據(jù)傳入的tag寫(xiě)入不同的值。如果我們?nèi)ゲ榭丛创a就會(huì)發(fā)現(xiàn),實(shí)際上ExecuteString()最終還是會(huì)調(diào)用ExecuteFuncString()。fasttemplate提供了一個(gè)標(biāo)準(zhǔn)的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))
}
}
標(biāo)準(zhǔn)的TagFunc實(shí)現(xiàn)也非常簡(jiǎn)單,就是從參數(shù)map[string]interface{}中取出對(duì)應(yīng)的值做相應(yīng)處理,如果是[]byte和string類(lèi)型,直接調(diào)用io.Writer的寫(xiě)入方法。如果是TagFunc類(lèi)型則直接調(diào)用該方法,將io.Writer和tag傳入。其他類(lèi)型直接panic拋出錯(cuò)誤。
如果模板中的tag在參數(shù)map[string]interface{}中不存在,有兩種處理方式:
直接忽略,相當(dāng)于替換成了空字符串 ""。標(biāo)準(zhǔn)的stdTagFunc就是這樣處理的;保留原始 tag。keepUnknownTagFunc就是做這個(gè)事情的。
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一樣,函數(shù)前半部分如果tag未找到。直接寫(xiě)入startTag + tag + endTag作為替換的值。
我們前面調(diào)用的ExecuteString()方法使用stdTagFunc,即直接將未識(shí)別的tag替換成空字符串。如果想保留未識(shí)別的tag,改為調(diào)用ExecuteStringStd()方法即可。該方法遇到未識(shí)別的tag會(huì)保留:
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)
}
參數(shù)中缺少age,運(yùn)行結(jié)果:
name: dj
age:
name: dj
age: {{age}}
帶io.Writer參數(shù)的方法
前面介紹的方法最后都是返回一個(gè)字符串。方法名中都有String:ExecuteString()/ExecuteFuncString()。
我們可以直接傳入一個(gè)io.Writer參數(shù),將結(jié)果字符串調(diào)用這個(gè)參數(shù)的Write()方法直接寫(xiě)入。這類(lèi)方法名中沒(méi)有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實(shí)現(xiàn)了io.Writer接口,可以直接傳入。結(jié)果直接寫(xiě)到os.Stdout中。運(yùn)行:
name: dj
age: 18
name: hjw
age: 20
源碼分析
首先看模板對(duì)象的結(jié)構(gòu)和創(chuàng)建:
// 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
}
模板創(chuàng)建之后會(huì)調(diào)用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
}
初始化做了下面這些事情:
記錄開(kāi)始和結(jié)束占位符; 解析模板,將文本和 tag切分開(kāi),分別存放在texts和tags切片中。后半段的for循環(huán)就是做的這個(gè)事情。
代碼細(xì)節(jié)點(diǎn):
先統(tǒng)計(jì)占位符一共多少個(gè),一次構(gòu)造對(duì)應(yīng)大小的文本和 tag切片,注意構(gòu)造正確的模板字符串文本切片一定比tag切片大 1。像這樣| text | tag | text | ... | tag | text |;為了避免內(nèi)存拷貝,使用 unsafeString2Bytes讓返回的字節(jié)切片直接指向string內(nèi)部地址。
看上面的介紹,貌似有很多方法。實(shí)際上核心的方法就一個(gè)ExecuteFunc()。其他的方法都是直接或間接地調(diào)用它:
// 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()方法構(gòu)造一個(gè)TagFunc調(diào)用ExecuteFunc(),內(nèi)部使用stdTagFunc:
func(w io.Writer, tag string) (int, error) {
return stdTagFunc(w, tag, m)
}
ExecuteStd()方法構(gòu)造一個(gè)TagFunc調(diào)用ExecuteFunc(),內(nèi)部使用keepUnknownTagFunc:
func(w io.Writer, tag string) (int, error) {
return keepUnknownTagFunc(w, t.startTag, t.endTag, tag, m)
}
ExecuteString()和ExecuteStringStd()方法調(diào)用ExecuteFuncString()方法,而ExecuteFuncString()方法又調(diào)用了ExecuteFuncStringWithErr()方法,ExecuteFuncStringWithErr()方法內(nèi)部使用bytebufferpool.Get()獲得一個(gè)bytebufferpoo.Buffer對(duì)象去調(diào)用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
}
整個(gè)邏輯也很清晰,for循環(huán)就是Write一個(gè)texts元素,以當(dāng)前的tag執(zhí)行TagFunc,索引 +1。最后寫(xiě)入最后一個(gè)texts元素,完成。大概是這樣:
| text | tag | text | tag | text | ... | tag | text |
注:ExecuteFuncStringWithErr()方法使用到了前面文章介紹的bytebufferpool,感興趣可以回去翻看。
總結(jié)
可以使用fasttemplate完成strings.Replace和fmt.Sprintf的任務(wù),而且fasttemplate靈活性更高。代碼清晰易懂,值得一看。
吐槽:關(guān)于命名,Execute()方法里面使用stdTagFunc,ExecuteStd()方法里面使用keepUnknownTagFunc方法。我想是不是把stdTagFunc改名為defaultTagFunc好一點(diǎn)?
大家如果發(fā)現(xiàn)好玩、好用的 Go 語(yǔ)言庫(kù),歡迎到 Go 每日一庫(kù) GitHub 上提交 issue??
參考
fasttemplate GitHub:github.com/valyala/fasttemplate Go 每日一庫(kù) GitHub:https://github.com/darjun/go-daily-lib
推薦閱讀
