简介
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)
}
- 定义模板字符串,应用
{{
和}}
示意占位符,占位符能够在创立模板的时候指定; - 调用
fasttemplate.New()
创立一个模板对象t
,传入开始和完结占位符; - 调用模板对象的
t.ExecuteString()
办法,传入参数。参数中有各个占位符对应的值。生成最终的字符串。
运行后果:
name: dj
age: 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.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{}
中取出对应的值做相应解决,如果是[]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: dj
age:
name: dj
age: {{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: 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
切离开,别离寄存在texts
和tags
切片中。后半段的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.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/darjun/go-daily-lib
我
我的博客:https://darjun.github.io
欢送关注我的微信公众号【GoUpUp】,独特学习,一起提高~
发表回复