简介

fasttemplate是一个比较简单、易用的小型模板库。fasttemplate的作者valyala另外还开源了不少优秀的库,如大名鼎鼎的fasthttp,前面介绍的bytebufferpool,还有一个重量级的模板库quicktemplatequicktemplate比标准库中的text/templatehtml/template要灵活和易用很多,后面会专门介绍它。今天要介绍的fasttemlate只专注于一块很小的领域——字符串替换。它的目标是为了替代strings.Replacefmt.Sprintf等方法,提供一个简单,易用,高性能的字符串替换方法。

本文首先介绍fasttemplate的用法,然后去看看源码实现的一些细节。

快速使用

本文代码使用 Go Modules。

创建目录并初始化:

  1. $ mkdir fasttemplate && cd fasttemplate
  2. $ go mod init github.com/go-quiz/go-daily-lib/fasttemplate

安装fasttemplate库:

  1. $ go get -u github.com/valyala/fasttemplate

编写代码:

  1. package main
  2. import (
  3. "fmt"
  4. "github.com/valyala/fasttemplate"
  5. )
  6. func main() {
  7. template := `name: {{name}}
  8. age: {{age}}`
  9. t := fasttemplate.New(template, "{{", "}}")
  10. s1 := t.ExecuteString(map[string]interface{}{
  11. "name": "dj",
  12. "age": "18",
  13. })
  14. s2 := t.ExecuteString(map[string]interface{}{
  15. "name": "hjw",
  16. "age": "20",
  17. })
  18. fmt.Println(s1)
  19. fmt.Println(s2)
  20. }
  • 定义模板字符串,使用{{}}表示占位符,占位符可以在创建模板的时候指定;
  • 调用fasttemplate.New()创建一个模板对象t,传入开始和结束占位符;
  • 调用模板对象的t.ExecuteString()方法,传入参数。参数中有各个占位符对应的值。生成最终的字符串。

运行结果:

  1. name: dj
  2. age: 18

我们可以自定义占位符,上面分别使用{{}}作为开始和结束占位符。我们可以换成[[]],只需要简单修改一下代码即可:

  1. template := `name: [[name]]
  2. age: [[age]]`
  3. t := fasttemplate.New(template, "[[", "]]")

另外,需要注意的是,传入参数的类型为map[string]interface{},但是fasttemplate只接受类型为[]bytestringTagFunc类型的值。这也是为什么上面的18要用双引号括起来的原因。

另一个需要注意的点,fasttemplate.New()返回一个模板对象,如果模板解析失败了,就会直接panic。如果想要自己处理错误,可以调用fasttemplate.NewTemplate()方法,该方法返回一个模板对象和一个错误。实际上,fasttemplate.New()内部就是调用fasttemplate.NewTemplate(),如果返回了错误,就panic

  1. // src/github.com/valyala/fasttemplate/template.go
  2. func New(template, startTag, endTag string) *Template {
  3. t, err := NewTemplate(template, startTag, endTag)
  4. if err != nil {
  5. panic(err)
  6. }
  7. return t
  8. }
  9. func NewTemplate(template, startTag, endTag string) (*Template, error) {
  10. var t Template
  11. err := t.Reset(template, startTag, endTag)
  12. if err != nil {
  13. return nil, err
  14. }
  15. return &t, nil
  16. }

这其实也是一种惯用法,对于不想处理错误的示例程序,直接panic有时也是一种选择。例如html.template标准库也提供了Must()方法,一般这样用,遇到解析失败就panic

  1. t := template.Must(template.New("name").Parse("html"))

占位符中间内部不要加空格!!!

占位符中间内部不要加空格!!!

占位符中间内部不要加空格!!!

快捷方式

使用fasttemplate.New()定义模板对象的方式,我们可以多次使用不同的参数去做替换。但是,有时候我们要做大量一次性的替换,每次都定义模板对象显得比较繁琐。fasttemplate也提供了一次性替换的方法:

  1. func main() {
  2. template := `name: [name]
  3. age: [age]`
  4. s := fasttemplate.ExecuteString(template, "[", "]", map[string]interface{}{
  5. "name": "dj",
  6. "age": "18",
  7. })
  8. fmt.Println(s)
  9. }

使用这种方式,我们需要同时传入模板字符串、开始占位符、结束占位符和替换参数。

TagFunc

fasttemplate提供了一个TagFunc,可以给替换增加一些逻辑。TagFunc是一个函数:

  1. type TagFunc func(w io.Writer, tag string) (int, error)

在执行替换的时候,fasttemplate针对每个占位符都会调用一次TagFunc函数,tag即占位符的名称。看下面程序:

  1. func main() {
  2. template := `name: {{name}}
  3. age: {{age}}`
  4. t := fasttemplate.New(template, "{{", "}}")
  5. s := t.ExecuteFuncString(func(w io.Writer, tag string) (int, error) {
  6. switch tag {
  7. case "name":
  8. return w.Write([]byte("dj"))
  9. case "age":
  10. return w.Write([]byte("18"))
  11. default:
  12. return 0, nil
  13. }
  14. })
  15. fmt.Println(s)
  16. }

这其实就是get-started示例程序的TagFunc版本,根据传入的tag写入不同的值。如果我们去查看源码就会发现,实际上ExecuteString()最终还是会调用ExecuteFuncString()fasttemplate提供了一个标准的TagFunc

  1. func (t *Template) ExecuteString(m map[string]interface{}) string {
  2. return t.ExecuteFuncString(func(w io.Writer, tag string) (int, error) { return stdTagFunc(w, tag, m) })
  3. }
  4. func stdTagFunc(w io.Writer, tag string, m map[string]interface{}) (int, error) {
  5. v := m[tag]
  6. if v == nil {
  7. return 0, nil
  8. }
  9. switch value := v.(type) {
  10. case []byte:
  11. return w.Write(value)
  12. case string:
  13. return w.Write([]byte(value))
  14. case TagFunc:
  15. return value(w, tag)
  16. default:
  17. panic(fmt.Sprintf("tag=%q contains unexpected value type=%#v. Expected []byte, string or TagFunc", tag, v))
  18. }
  19. }

标准的TagFunc实现也非常简单,就是从参数map[string]interface{}中取出对应的值做相应处理,如果是[]bytestring类型,直接调用io.Writer的写入方法。如果是TagFunc类型则直接调用该方法,将io.Writertag传入。其他类型直接panic抛出错误。

如果模板中的tag在参数map[string]interface{}中不存在,有两种处理方式:

  • 直接忽略,相当于替换成了空字符串""。标准的stdTagFunc就是这样处理的;
  • 保留原始tagkeepUnknownTagFunc就是做这个事情的。

keepUnknownTagFunc代码如下:

  1. func keepUnknownTagFunc(w io.Writer, startTag, endTag, tag string, m map[string]interface{}) (int, error) {
  2. v, ok := m[tag]
  3. if !ok {
  4. if _, err := w.Write(unsafeString2Bytes(startTag)); err != nil {
  5. return 0, err
  6. }
  7. if _, err := w.Write(unsafeString2Bytes(tag)); err != nil {
  8. return 0, err
  9. }
  10. if _, err := w.Write(unsafeString2Bytes(endTag)); err != nil {
  11. return 0, err
  12. }
  13. return len(startTag) + len(tag) + len(endTag), nil
  14. }
  15. if v == nil {
  16. return 0, nil
  17. }
  18. switch value := v.(type) {
  19. case []byte:
  20. return w.Write(value)
  21. case string:
  22. return w.Write([]byte(value))
  23. case TagFunc:
  24. return value(w, tag)
  25. default:
  26. panic(fmt.Sprintf("tag=%q contains unexpected value type=%#v. Expected []byte, string or TagFunc", tag, v))
  27. }
  28. }

后半段处理与stdTagFunc一样,函数前半部分如果tag未找到。直接写入startTag + tag + endTag作为替换的值。

我们前面调用的ExecuteString()方法使用stdTagFunc,即直接将未识别的tag替换成空字符串。如果想保留未识别的tag,改为调用ExecuteStringStd()方法即可。该方法遇到未识别的tag会保留:

  1. func main() {
  2. template := `name: {{name}}
  3. age: {{age}}`
  4. t := fasttemplate.New(template, "{{", "}}")
  5. m := map[string]interface{}{"name": "dj"}
  6. s1 := t.ExecuteString(m)
  7. fmt.Println(s1)
  8. s2 := t.ExecuteStringStd(m)
  9. fmt.Println(s2)
  10. }

参数中缺少age,运行结果:

  1. name: dj
  2. age:
  3. name: dj
  4. age: {{age}}

io.Writer参数的方法

前面介绍的方法最后都是返回一个字符串。方法名中都有StringExecuteString()/ExecuteFuncString()

我们可以直接传入一个io.Writer参数,将结果字符串调用这个参数的Write()方法直接写入。这类方法名中没有StringExecute()/ExecuteFunc()

  1. func main() {
  2. template := `name: {{name}}
  3. age: {{age}}`
  4. t := fasttemplate.New(template, "{{", "}}")
  5. t.Execute(os.Stdout, map[string]interface{}{
  6. "name": "dj",
  7. "age": "18",
  8. })
  9. fmt.Println()
  10. t.ExecuteFunc(os.Stdout, func(w io.Writer, tag string) (int, error) {
  11. switch tag {
  12. case "name":
  13. return w.Write([]byte("hjw"))
  14. case "age":
  15. return w.Write([]byte("20"))
  16. }
  17. return 0, nil
  18. })
  19. }

由于os.Stdout实现了io.Writer接口,可以直接传入。结果直接写到os.Stdout中。运行:

  1. name: dj
  2. age: 18
  3. name: hjw
  4. age: 20

源码分析

首先看模板对象的结构和创建:

  1. // src/github.com/valyala/fasttemplate/template.go
  2. type Template struct {
  3. template string
  4. startTag string
  5. endTag string
  6. texts [][]byte
  7. tags []string
  8. byteBufferPool bytebufferpool.Pool
  9. }
  10. func NewTemplate(template, startTag, endTag string) (*Template, error) {
  11. var t Template
  12. err := t.Reset(template, startTag, endTag)
  13. if err != nil {
  14. return nil, err
  15. }
  16. return &t, nil
  17. }

模板创建之后会调用Reset()方法初始化:

  1. func (t *Template) Reset(template, startTag, endTag string) error {
  2. t.template = template
  3. t.startTag = startTag
  4. t.endTag = endTag
  5. t.texts = t.texts[:0]
  6. t.tags = t.tags[:0]
  7. if len(startTag) == 0 {
  8. panic("startTag cannot be empty")
  9. }
  10. if len(endTag) == 0 {
  11. panic("endTag cannot be empty")
  12. }
  13. s := unsafeString2Bytes(template)
  14. a := unsafeString2Bytes(startTag)
  15. b := unsafeString2Bytes(endTag)
  16. tagsCount := bytes.Count(s, a)
  17. if tagsCount == 0 {
  18. return nil
  19. }
  20. if tagsCount+1 > cap(t.texts) {
  21. t.texts = make([][]byte, 0, tagsCount+1)
  22. }
  23. if tagsCount > cap(t.tags) {
  24. t.tags = make([]string, 0, tagsCount)
  25. }
  26. for {
  27. n := bytes.Index(s, a)
  28. if n < 0 {
  29. t.texts = append(t.texts, s)
  30. break
  31. }
  32. t.texts = append(t.texts, s[:n])
  33. s = s[n+len(a):]
  34. n = bytes.Index(s, b)
  35. if n < 0 {
  36. return fmt.Errorf("Cannot find end tag=%q in the template=%q starting from %q", endTag, template, s)
  37. }
  38. t.tags = append(t.tags, unsafeBytes2String(s[:n]))
  39. s = s[n+len(b):]
  40. }
  41. return nil
  42. }

初始化做了下面这些事情:

  • 记录开始和结束占位符;
  • 解析模板,将文本和tag切分开,分别存放在textstags切片中。后半段的for循环就是做的这个事情。

代码细节点:

  • 先统计占位符一共多少个,一次构造对应大小的文本和tag切片,注意构造正确的模板字符串文本切片一定比tag切片大 1。像这样| text | tag | text | ... | tag | text |
  • 为了避免内存拷贝,使用unsafeString2Bytes让返回的字节切片直接指向string内部地址。

看上面的介绍,貌似有很多方法。实际上核心的方法就一个ExecuteFunc()。其他的方法都是直接或间接地调用它:

  1. // src/github.com/valyala/fasttemplate/template.go
  2. func (t *Template) Execute(w io.Writer, m map[string]interface{}) (int64, error) {
  3. return t.ExecuteFunc(w, func(w io.Writer, tag string) (int, error) { return stdTagFunc(w, tag, m) })
  4. }
  5. func (t *Template) ExecuteStd(w io.Writer, m map[string]interface{}) (int64, error) {
  6. return t.ExecuteFunc(w, func(w io.Writer, tag string) (int, error) { return keepUnknownTagFunc(w, t.startTag, t.endTag, tag, m) })
  7. }
  8. func (t *Template) ExecuteFuncString(f TagFunc) string {
  9. s, err := t.ExecuteFuncStringWithErr(f)
  10. if err != nil {
  11. panic(fmt.Sprintf("unexpected error: %s", err))
  12. }
  13. return s
  14. }
  15. func (t *Template) ExecuteFuncStringWithErr(f TagFunc) (string, error) {
  16. bb := t.byteBufferPool.Get()
  17. if _, err := t.ExecuteFunc(bb, f); err != nil {
  18. bb.Reset()
  19. t.byteBufferPool.Put(bb)
  20. return "", err
  21. }
  22. s := string(bb.Bytes())
  23. bb.Reset()
  24. t.byteBufferPool.Put(bb)
  25. return s, nil
  26. }
  27. func (t *Template) ExecuteString(m map[string]interface{}) string {
  28. return t.ExecuteFuncString(func(w io.Writer, tag string) (int, error) { return stdTagFunc(w, tag, m) })
  29. }
  30. func (t *Template) ExecuteStringStd(m map[string]interface{}) string {
  31. return t.ExecuteFuncString(func(w io.Writer, tag string) (int, error) { return keepUnknownTagFunc(w, t.startTag, t.endTag, tag, m) })
  32. }

Execute()方法构造一个TagFunc调用ExecuteFunc(),内部使用stdTagFunc

  1. func(w io.Writer, tag string) (int, error) {
  2. return stdTagFunc(w, tag, m)
  3. }

ExecuteStd()方法构造一个TagFunc调用ExecuteFunc(),内部使用keepUnknownTagFunc

  1. func(w io.Writer, tag string) (int, error) {
  2. return keepUnknownTagFunc(w, t.startTag, t.endTag, tag, m)
  3. }

ExecuteString()ExecuteStringStd()方法调用ExecuteFuncString()方法,而ExecuteFuncString()方法又调用了ExecuteFuncStringWithErr()方法,ExecuteFuncStringWithErr()方法内部使用bytebufferpool.Get()获得一个bytebufferpoo.Buffer对象去调用ExecuteFunc()方法。所以核心就是ExecuteFunc()方法:

  1. func (t *Template) ExecuteFunc(w io.Writer, f TagFunc) (int64, error) {
  2. var nn int64
  3. n := len(t.texts) - 1
  4. if n == -1 {
  5. ni, err := w.Write(unsafeString2Bytes(t.template))
  6. return int64(ni), err
  7. }
  8. for i := 0; i < n; i++ {
  9. ni, err := w.Write(t.texts[i])
  10. nn += int64(ni)
  11. if err != nil {
  12. return nn, err
  13. }
  14. ni, err = f(w, t.tags[i])
  15. nn += int64(ni)
  16. if err != nil {
  17. return nn, err
  18. }
  19. }
  20. ni, err := w.Write(t.texts[n])
  21. nn += int64(ni)
  22. return nn, err
  23. }

整个逻辑也很清晰,for循环就是Write一个texts元素,以当前的tag执行TagFunc,索引 +1。最后写入最后一个texts元素,完成。大概是这样:

  1. | text | tag | text | tag | text | ... | tag | text |

注:ExecuteFuncStringWithErr()方法使用到了前面文章介绍的bytebufferpool,感兴趣可以回去翻看。

总结

可以使用fasttemplate完成strings.Replacefmt.Sprintf的任务,而且fasttemplate灵活性更高。代码清晰易懂,值得一看。

吐槽:关于命名,Execute()方法里面使用stdTagFuncExecuteStd()方法里面使用keepUnknownTagFunc方法。我想是不是把stdTagFunc改名为defaultTagFunc好一点?

大家如果发现好玩、好用的 Go 语言库,欢迎到 Go 每日一库 GitHub 上提交 issue😄

参考

  1. fasttemplate GitHub:github.com/valyala/fasttemplate
  2. Go 每日一库 GitHub:https://github.com/go-quiz/go-daily-lib