1

Go 每日一库之 fasttemplate

 3 years ago
source link: https://segmentfault.com/a/1190000040058101
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Go 每日一库之 fasttemplate

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

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

本文代码使用 Go Modules。

创建目录并初始化:

$ 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)
}
  • 定义模板字符串,使用{{}}表示占位符,占位符可以在创建模板的时候指定;
  • 调用fasttemplate.New()创建一个模板对象t,传入开始和结束占位符;
  • 调用模板对象的t.ExecuteString()方法,传入参数。参数中有各个占位符对应的值。生成最终的字符串。

运行结果:

name: dj
age: 18

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

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

另外,需要注意的是,传入参数的类型为map[string]interface{},但是fasttemplate只接受类型为[]bytestringTagFunc类型的值。这也是为什么上面的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{}中取出对应的值做相应处理,如果是[]bytestring类型,直接调用io.Writer的写入方法。如果是TagFunc类型则直接调用该方法,将io.Writertag传入。其他类型直接panic抛出错误。

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

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

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参数的方法

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

我们可以直接传入一个io.Writer参数,将结果字符串调用这个参数的Write()方法直接写入。这类方法名中没有StringExecute()/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切分开,分别存放在textstags切片中。后半段的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.Replacefmt.Sprintf的任务,而且fasttemplate灵活性更高。代码清晰易懂,值得一看。

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

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

我的博客:https://darjun.github.io

欢迎关注我的微信公众号【GoUpUp】,共同学习,一起进步~


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK