简介
fasttemplate是一个比较简单、易用的小型模板库。fasttemplate的作者valyala另外还开源了不少优秀的库,如大名鼎鼎的fasthttp,前面介绍的bytebufferpool,还有一个重量级的模板库quicktemplate。quicktemplate比标准库中的text/template和html/template要灵活和易用很多,后面会专门介绍它。今天要介绍的fasttemlate只专注于一块很小的领域——字符串替换。它的目标是为了替代strings.Replace、fmt.Sprintf等方法,提供一个简单,易用,高性能的字符串替换方法。
本文首先介绍fasttemplate的用法,然后去看看源码实现的一些细节。
快速使用
本文代码使用 Go Modules。
创建目录并初始化:
$ mkdir fasttemplate && cd fasttemplate$ go mod init github.com/go-quiz/go-daily-lib/fasttemplate
安装fasttemplate库:
$ go get -u github.com/valyala/fasttemplate
编写代码:
package mainimport ("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: djage: 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.gofunc 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 Templateerr := 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: djage:name: djage: {{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: djage: 18name: hjwage: 20
源码分析
首先看模板对象的结构和创建:
// src/github.com/valyala/fasttemplate/template.gotype Template struct {template stringstartTag stringendTag stringtexts [][]bytetags []stringbyteBufferPool bytebufferpool.Pool}func NewTemplate(template, startTag, endTag string) (*Template, error) {var t Templateerr := 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 = templatet.startTag = startTagt.endTag = endTagt.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.gofunc (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 int64n := len(t.texts) - 1if 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😄
参考
- fasttemplate GitHub:github.com/valyala/fasttemplate
- Go 每日一库 GitHub:https://github.com/go-quiz/go-daily-lib
