介绍
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/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)
}
运行结果:
name: dj
age: 18
我们可以自定义占位符,分别使用 {{ 和 }} 作为开始和结束占位符。 我们可以将其替换为[[和]],只需简单更改代码即可:
template := `name: [[name]]
age: [[age]]`
t := fasttemplate.New(template, "[[", "]]")
另外,需要注意的是,传入参数的类型为map[string]interface{}html空格占位符,但fasttemplate只接受[]byte、string和TagFunc类型的值。 这也是为什么里面的18要用双冒号括起来的原因。
另一点需要注意的是 fasttemplate.New() 返回一个模板对象。 如果模板解析失败html空格占位符,会直接panic。 如果你想自己处理错误,可以调用 fasttemplate.NewTemplate() 方法,该方法返回一个模板对象和一个错误。 事实上,fasttemplate.New()内部调用了fasttemplate.NewTemplate()。 如果返回错误,则恐慌:
// 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
}
这也可能是一种常见用法。 例如,不想处理错误的程序,有时可以选择直接恐慌。 例如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)
}
使用这些方法,我们需要同时传入模板字符串、开始占位符、结束占位符和替换参数。
标签函数
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)
}
这似乎是入门示例程序的TagFunc版本,根据传入的标签写入不同的值。如果我们查看源代码,我们会发现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的写入方法。作家。 如果是TagFunc类型,则直接调用该方法,传入io.Writer和tag。 其他类型直接恐慌并抛出错误。
如果参数map[string]interface{}中不存在模板中的标签,有两种处理方式:
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 相同。 直接写入startTag + tag + endTag作为替换值。
上面我们调用的ExecuteString()方法使用了stdTagFunc,它直接用空字符串替换无法识别的标签。 如果您想保留无法识别的标签,只需调用 ExecuteStringStd() 方法即可。 该方法遇到无法识别的标签时,会被保留:
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)
}
参数中没有年龄,运行结果为:
name: dj
age:
name: dj
age: {{age}}
带 io.Writer 参数的方法
上面描述的方法都在末尾返回一个字符串。 方法名称中包含字符串: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: 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
}
初始化执行以下操作:
代码详情:
看介绍,好像有很多技巧。 其实核心方法就是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 循环写入文本元素,使用当前标签执行 TagFunc,并索引 +1。 最后写下最后一个文本元素并完成。 大概是这样的:
| text | tag | text | tag | text | ... | tag | text |
注意:ExecuteFuncStringWithErr()方法使用了上一篇文章中介绍的bytebufferpool。 如果您有兴趣,可以回来阅读。
总结
fasttemplate可以用来完成strings.Replace和fmt.Sprintf的任务,而且fasttemplate更加灵活。 代码清晰易懂,值得一看。
吐槽:关于命名,在Execute()方法上使用stdTagFunc,在ExecuteStd()方法上使用keepUnknownTagFunc技巧。 我认为将 stdTagFunc 重命名为 defaultTagFunc 会更好吗?
如果您发现有趣且好用的 Go 语言库,欢迎在 Go Daily Library GitHub 上提交 Issue
参考
快速模板 GitHub:github.com/valyala/fasttemplate
Go 每日图书馆 GitHub: