簡介
fasttemplate
是一個(gè)比較簡單虹蒋、易用的小型模板庫椭赋。fasttemplate
的作者valyala另外還開源了不少優(yōu)秀的庫功舀,如大名鼎鼎的fasthttp
令漂,前面介紹的bytebufferpool
挂据,還有一個(gè)重量級(jí)的模板庫quicktemplate
胖喳。quicktemplate
比標(biāo)準(zhǔn)庫中的text/template
和html/template
要靈活和易用很多泡躯,后面會(huì)專門介紹它。今天要介紹的fasttemlate
只專注于一塊很小的領(lǐng)域——字符串替換丽焊。它的目標(biāo)是為了替代strings.Replace
较剃、fmt.Sprintf
等方法,提供一個(gè)簡單技健,易用写穴,高性能的字符串替換方法。
本文首先介紹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
庫:
$ go get -u github.com/valyala/fasttemplate
編寫代碼:
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
降传,傳入開始和結(jié)束占位符篷朵; - 調(diào)用模板對(duì)象的
t.ExecuteString()
方法,傳入?yún)?shù)婆排。參數(shù)中有各個(gè)占位符對(duì)應(yīng)的值声旺。生成最終的字符串。
運(yùn)行結(jié)果:
name: dj
age: 18
我們可以自定義占位符泽论,上面分別使用{{
和}}
作為開始和結(jié)束占位符艾少。我們可以換成[[
和]]
,只需要簡單修改一下代碼即可:
template := `name: [[name]]
age: [[age]]`
t := fasttemplate.New(template, "[[", "]]")
另外翼悴,需要注意的是缚够,傳入?yún)?shù)的類型為map[string]interface{}
幔妨,但是fasttemplate
只接受類型為[]byte
、string
和TagFunc
類型的值谍椅。這也是為什么上面的18
要用雙引號(hào)括起來的原因误堡。
另一個(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)庫也提供了Must()
方法谈飒,一般這樣用岂座,遇到解析失敗就panic
:
t := template.Must(template.New("name").Parse("html"))
占位符中間內(nèi)部不要加空格!:即搿费什!
占位符中間內(nèi)部不要加空格!H拷椤吕喘!
占位符中間內(nèi)部不要加空格!P躺!氯质!
快捷方式
使用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í)傳入模板字符串、開始占位符吴超、結(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
即占位符的名稱〕氯瑁看下面程序:
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
寫入不同的值。如果我們?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)也非常簡單,就是從參數(shù)map[string]interface{}
中取出對(duì)應(yīng)的值做相應(yīng)處理利赋,如果是[]byte
和string
類型水评,直接調(diào)用io.Writer
的寫入方法。如果是TagFunc
類型則直接調(diào)用該方法隐砸,將io.Writer
和tag
傳入之碗。其他類型直接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
未找到。直接寫入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()
方法直接寫入离钝。這類方法名中沒有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é)果直接寫到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
}
初始化做了下面這些事情:
- 記錄開始和結(jié)束占位符慧域;
- 解析模板,將文本和
tag
切分開浪读,分別存放在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月杉。最后寫入最后一個(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 語言庫炊甲,歡迎到 Go 每日一庫 GitHub 上提交 issue??
參考
- fasttemplate GitHub:github.com/valyala/fasttemplate
- Go 每日一庫 GitHub:https://github.com/darjun/go-daily-lib
我
歡迎關(guān)注我的微信公眾號(hào)【GoUpUp】泥彤,共同學(xué)習(xí),一起進(jìn)步~