简介

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

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

快速使用

本文代码使用 Go Modules。

创建目录并初始化:

 
bash

复制代码
$ mkdir fasttemplate && cd fasttemplate
$ go mod init github.com/go-quiz/go-daily-lib/fasttemplate

安装fasttemplate库:

 
bash

复制代码
$ go get -u github.com/valyala/fasttemplate

编写代码:

 
go

复制代码
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()方法,传入参数。参数中有各个占位符对应的值。生成最终的字符串。

运行结果:

 
bash

复制代码
name: dj
age: 18

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

 
go

复制代码
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

 
go

复制代码
// 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

 
go

复制代码
t := template.Must(template.New("name").Parse("html"))

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

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

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

快捷方式

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

 
go

复制代码
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是一个函数:

 
go

复制代码
type TagFunc func(w io.Writer, tag string) (int, error)

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

 
go

复制代码
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

 
go

复制代码
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代码如下:

 
go

复制代码
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会保留:

 
go

复制代码
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,运行结果:

 
bash

复制代码
name: dj
age:
name: dj
age: {{age}}

io.Writer参数的方法

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

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

 
go

复制代码
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中。运行:

 
bash

复制代码
name: dj
age: 18
name: hjw
age: 20

源码分析

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

 
go

复制代码
// 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()方法初始化:

 
go

复制代码
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()。其他的方法都是直接或间接地调用它:

 
go

复制代码
// 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

 
go

复制代码
func(w io.Writer, tag string) (int, error) {
return stdTagFunc(w, tag, m)
}

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

 
go

复制代码
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()方法:

 
go

复制代码
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元素,完成。大概是这样:

 
go

复制代码
| text | tag | text | tag | text | ... | tag | text |

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

总结

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

 

golang模板库之fasttemplate的更多相关文章

  1. golang常用库:cli命令行/应用程序生成工具-cobra使用

    golang常用库:cli命令行/应用程序生成工具-cobra使用 一.Cobra 介绍 我前面有一篇文章介绍了配置文件解析库 Viper 的使用,这篇介绍 Cobra 的使用,你猜的没错,这 2 个 ...

  2. STL标准模板库(简介)

    标准模板库(STL,Standard Template Library)是C++标准库的重要组成部分,包含了诸多在计算机科学领域里所常见的基本数据结构和基本算法,为广大C++程序员提供了一个可扩展的应 ...

  3. c++转载系列 std::vector模板库用法介绍

    来源:http://blog.csdn.net/phoebin/article/details/3864590 介绍 这篇文章的目的是为了介绍std::vector,如何恰当地使用它们的成员函数等操作 ...

  4. Handlebars模板库浅析

    Handlebars模板库简单介绍 Handlebars是JavaScript一个语义模板库,通过对view(模板)和data(ajax请求的数据,一般是json)的分离来快速构建Web模板.它采用& ...

  5. 【转】C++标准库和标准模板库

    C++强大的功能来源于其丰富的类库及库函数资源.C++标准库的内容总共在50个标准头文件中定义.在C++开发中,要尽可能地利用标准库完成.这样做的直接好处包括:(1)成本:已经作为标准提供,何苦再花费 ...

  6. STL标准模板库介绍

    1. STL介绍 标准模板库STL是当今每个从事C++编程的人需要掌握的技术,所有很有必要总结下 本文将介绍STL并探讨它的三个主要概念:容器.迭代器.算法. STL的最大特点就是: 数据结构和算法的 ...

  7. c++模板库(简介)

    目 录 STL 简介 ......................................................................................... ...

  8. 【c++】标准模板库STL入门简介与常见用法

    一.STL简介 1.什么是STL STL(Standard Template Library)标准模板库,主要由容器.迭代器.算法.函数对象.内存分配器和适配器六大部分组成.STL已是标准C++的一部 ...

  9. C++——string类和标准模板库

    一.string类 1.构造函数 string实际上是basic_string<char>的一个typedef,同时省略了与内存管理相关的参数.size_type是一个依赖于实现的整型,是 ...

  10. STL 简介,标准模板库

    这篇文章是关于C++语言的一个新的扩展--标准模板库的(Standard Template Library),也叫STL.  当我第一次打算写一篇关于STL的文章的时候,我不得不承认我当时低估了这个话 ...

随机推荐

  1. MySQL服务端innodb_buffer_pool_size配置参数

    innodb_buffer_pool_size是什么? innodb_buffer_pool是 InnoDB 缓冲池,是一个内存区域保存缓存的 InnoDB 数据为表.索引和其他辅助缓冲区.innod ...

  2. 知识增强深度学习及其应用:综述《Knowledge-augmented Deep Learning and Its Applications: A Survey》(下)

    论文:Knowledge-augmented Deep Learning and Its Applications: A Survey GitHub: arXiv上的论文. (接着来) 4 用经验知识 ...

  3. ASP.NET Core – Razor Pages Routing

    前言 之前有提过, MVC 和 Razor Pages 最大的区别就在 Routing 上. Razor Pages 的结构是 route, page, model route match to pa ...

  4. CSS – RWD (Responsive Web Design) 概念篇

    介绍 Only PC 以前是没有手机的, 只有电脑, 所以做开发, 只需要开发电脑版本就可以了. Mobile Version 后来手机诞生, 有钱的公司就做两个版本, 一个手机版, 一个电脑版. 没 ...

  5. Servlet——Request请求转发

    Request请求转发       特点:     

  6. linux、unix软链接注意事项

    前言 在使用linux过程中,经常使用到软链接(类似windows快捷方式): 创建软链接之后,删除时不注意就会出现到问题 先说结论 删除软链接,确实是使用rm进行删除:但是有个小细节必须要特别注意! ...

  7. 大一下的acm生活

    在一个名气不大的211学校刷题的日常. 感觉这些算法题好难啊! 最近有好多实验室要招新,不知道该怎么办,自己只想就业,并不想升学,好烦! 真枯燥,好无聊. 现在要学习相关的网页设计和网站建设,例如配色 ...

  8. Linux命令每天都要使用,但又太长记不住怎么办?教你1个方法

    序言各位好啊,我是会编程的蜗牛,作为java开发者 ,我们肯定会与linux服务器打交道,关于linux服务器的连接工具,可以参考我的文章Tabby,一款老外都在用的 SSH工具,竟然还支持网页操作~ ...

  9. 可持久化线段————主席树(洛谷p3834)

    洛谷P3834 可持久化线段树 2 问题描述: 给定n各整数构成的序列,求指定区间[L,R]内的第k小值(求升序排序后从左往右数第k个整数的数值) 输入: 第一行输入两个整数n,m,分别代表序列长度n ...

  10. MMU和SMMU IOMMU使用场景和区别,SMMU技术与cache

    1.各种MMU MMU是memory manage unit 内存管理单元: SMMU是system memory manage unit 系统内存管理单元: IOMMU和SMMU的功能基本相同,只是 ...