<article class=“article fmt article-content”><h2>简介</h2><p><code>fasttemplate</code>是一个比较简单、易用的小型模板库。<code>fasttemplate</code>的作者valyala另外还开源了不少优良的库,如赫赫有名的<code>fasthttp</code>,后面介绍的<code>bytebufferpool</code>,还有一个重量级的模板库<code>quicktemplate</code>。<code>quicktemplate</code>比规范库中的<code>text/template</code>和<code>html/template</code>要灵便和易用很多,前面会专门介绍它。明天要介绍的<code>fasttemlate</code>只专一于一块很小的畛域——字符串替换。它的指标是为了代替<code>strings.Replace</code>、<code>fmt.Sprintf</code>等办法,提供一个简略,易用,高性能的字符串替换办法。</p><p>本文首先介绍<code>fasttemplate</code>的用法,而后去看看源码实现的一些细节。</p><h2>疾速应用</h2><p>本文代码应用 Go Modules。</p><p>创立目录并初始化:</p><pre><code class=“cmd”>$ mkdir fasttemplate && cd fasttemplate$ go mod init github.com/darjun/go-daily-lib/fasttemplate</code></pre><p>装置<code>fasttemplate</code>库:</p><pre><code class=“cmd”>$ go get -u github.com/valyala/fasttemplate</code></pre><p>编写代码:</p><pre><code class=“golang”>package mainimport ( “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)}</code></pre><ul><li>定义模板字符串,应用<code>{{</code>和<code>}}</code>示意占位符,占位符能够在创立模板的时候指定;</li><li>调用<code>fasttemplate.New()</code>创立一个模板对象<code>t</code>,传入开始和完结占位符;</li><li>调用模板对象的<code>t.ExecuteString()</code>办法,传入参数。参数中有各个占位符对应的值。生成最终的字符串。</li></ul><p>运行后果:</p><pre><code class=“cmd”>name: djage: 18</code></pre><p>咱们能够自定义占位符,下面别离应用<code>{{</code>和<code>}}</code>作为开始和完结占位符。咱们能够换成<code>[[</code>和<code>]]</code>,只须要简略批改一下代码即可:</p><pre><code class=“golang”>template := name: [[name]]age: [[age]]t := fasttemplate.New(template, “[[”, “]]”)</code></pre><p>另外,须要留神的是,传入参数的类型为<code>map[string]interface{}</code>,然而<code>fasttemplate</code>只承受类型为<code>[]byte</code>、<code>string</code>和<code>TagFunc</code>类型的值。这也是为什么下面的<code>18</code>要用双引号括起来的起因。</p><p>另一个须要留神的点,<code>fasttemplate.New()</code>返回一个模板对象,如果模板解析失败了,就会间接<code>panic</code>。如果想要本人处理错误,能够调用<code>fasttemplate.NewTemplate()</code>办法,该办法返回一个模板对象和一个谬误。实际上,<code>fasttemplate.New()</code>外部就是调用<code>fasttemplate.NewTemplate()</code>,如果返回了谬误,就<code>panic</code>:</p><pre><code class=“golang”>// src/github.com/valyala/fasttemplate/template.gofunc 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}</code></pre><p><strong>这其实也是一种习用法,对于不想处理错误的示例程序,间接<code>panic</code>有时也是一种抉择</strong>。例如<code>html.template</code>规范库也提供了<code>Must()</code>办法,个别这样用,遇到解析失败就<code>panic</code>:</p><pre><code class=“golang”>t := template.Must(template.New(“name”).Parse(“html”))</code></pre><p><strong>占位符两头外部不要加空格!!!</strong></p><p><strong>占位符两头外部不要加空格!!!</strong></p><p><strong>占位符两头外部不要加空格!!!</strong></p><h2>快捷方式</h2><p>应用<code>fasttemplate.New()</code>定义模板对象的形式,咱们能够屡次应用不同的参数去做替换。然而,有时候咱们要做大量一次性的替换,每次都定义模板对象显得比拟繁琐。<code>fasttemplate</code>也提供了一次性替换的办法:</p><pre><code class=“golang”>func main() { template := name: [name]age: [age] s := fasttemplate.ExecuteString(template, “[”, “]”, map[string]interface{}{ “name”: “dj”, “age”: “18”, }) fmt.Println(s)}</code></pre><p>应用这种形式,咱们须要同时传入模板字符串、开始占位符、完结占位符和替换参数。</p><h2><code>TagFunc</code></h2><p><code>fasttemplate</code>提供了一个<code>TagFunc</code>,能够给替换减少一些逻辑。<code>TagFunc</code>是一个函数:</p><pre><code class=“golang”>type TagFunc func(w io.Writer, tag string) (int, error)</code></pre><p>在执行替换的时候,<code>fasttemplate</code>针对每个占位符都会调用一次<code>TagFunc</code>函数,<code>tag</code>即占位符的名称。看上面程序:</p><pre><code class=“golang”>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)}</code></pre><p>这其实就是<code>get-started</code>示例程序的<code>TagFunc</code>版本,依据传入的<code>tag</code>写入不同的值。如果咱们去查看源码就会发现,实际上<code>ExecuteString()</code>最终还是会调用<code>ExecuteFuncString()</code>。<code>fasttemplate</code>提供了一个规范的<code>TagFunc</code>:</p><pre><code class=“golang”>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)) }}</code></pre><p>规范的<code>TagFunc</code>实现也非常简单,就是从参数<code>map[string]interface{}</code>中取出对应的值做相应解决,如果是<code>[]byte</code>和<code>string</code>类型,间接调用<code>io.Writer</code>的写入办法。如果是<code>TagFunc</code>类型则间接调用该办法,将<code>io.Writer</code>和<code>tag</code>传入。其余类型间接<code>panic</code>抛出谬误。</p><p>如果模板中的<code>tag</code>在参数<code>map[string]interface{}</code>中不存在,有两种解决形式:</p><ul><li>间接疏忽,相当于替换成了空字符串<code>""</code>。规范的<code>stdTagFunc</code>就是这样解决的;</li><li>保留原始<code>tag</code>。<code>keepUnknownTagFunc</code>就是做这个事件的。</li></ul><p><code>keepUnknownTagFunc</code>代码如下:</p><pre><code class=“golang”>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)) }}</code></pre><p>后半段解决与<code>stdTagFunc</code>一样,函数前半部分如果<code>tag</code>未找到。间接写入<code>startTag</code> + <code>tag</code> + <code>endTag</code>作为替换的值。</p><p>咱们后面调用的<code>ExecuteString()</code>办法应用<code>stdTagFunc</code>,即间接将未辨认的<code>tag</code>替换成空字符串。如果想保留未辨认的<code>tag</code>,改为调用<code>ExecuteStringStd()</code>办法即可。该办法遇到未辨认的<code>tag</code>会保留:</p><pre><code class=“golang”>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)}</code></pre><p>参数中短少<code>age</code>,运行后果:</p><pre><code class=“cmd”>name: djage:name: djage: {{age}}</code></pre><h2>带<code>io.Writer</code>参数的办法</h2><p>后面介绍的办法最初都是返回一个字符串。办法名中都有<code>String</code>:<code>ExecuteString()/ExecuteFuncString()</code>。</p><p>咱们能够间接传入一个<code>io.Writer</code>参数,将后果字符串调用这个参数的<code>Write()</code>办法间接写入。这类办法名中没有<code>String</code>:<code>Execute()/ExecuteFunc()</code>:</p><pre><code class=“golang”>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 })}</code></pre><p>因为<code>os.Stdout</code>实现了<code>io.Writer</code>接口,能够间接传入。后果间接写到<code>os.Stdout</code>中。运行:</p><pre><code class=“cmd”>name: djage: 18name: hjwage: 20</code></pre><h2>源码剖析</h2><p>首先看模板对象的构造和创立:</p><pre><code class=“golang”>// src/github.com/valyala/fasttemplate/template.gotype 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}</code></pre><p>模板创立之后会调用<code>Reset()</code>办法初始化:</p><pre><code class=“golang”>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}</code></pre><p>初始化做了上面这些事件:</p><ul><li>记录开始和完结占位符;</li><li>解析模板,将文本和<code>tag</code>切离开,别离寄存在<code>texts</code>和<code>tags</code>切片中。后半段的<code>for</code>循环就是做的这个事件。</li></ul><p>代码细节点:</p><ul><li>先统计占位符一共多少个,一次结构对应大小的文本和<code>tag</code>切片,留神结构正确的模板字符串文本切片肯定比<code>tag</code>切片大 1。像这样<code>| text | tag | text | … | tag | text |</code>;</li><li>为了防止内存拷贝,应用<code>unsafeString2Bytes</code>让返回的字节切片间接指向<code>string</code>外部地址。</li></ul><p>看下面的介绍,貌似有很多办法。实际上外围的办法就一个<code>ExecuteFunc()</code>。其余的办法都是间接或间接地调用它:</p><pre><code class=“golang”>// src/github.com/valyala/fasttemplate/template.gofunc (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) })}</code></pre><p><code>Execute()</code>办法结构一个<code>TagFunc</code>调用<code>ExecuteFunc()</code>,外部应用<code>stdTagFunc</code>:</p><pre><code class=“golang”>func(w io.Writer, tag string) (int, error) { return stdTagFunc(w, tag, m)}</code></pre><p><code>ExecuteStd()</code>办法结构一个<code>TagFunc</code>调用<code>ExecuteFunc()</code>,外部应用<code>keepUnknownTagFunc</code>:</p><pre><code class=“golang”>func(w io.Writer, tag string) (int, error) { return keepUnknownTagFunc(w, t.startTag, t.endTag, tag, m)}</code></pre><p><code>ExecuteString()</code>和<code>ExecuteStringStd()</code>办法调用<code>ExecuteFuncString()</code>办法,而<code>ExecuteFuncString()</code>办法又调用了<code>ExecuteFuncStringWithErr()</code>办法,<code>ExecuteFuncStringWithErr()</code>办法外部应用<code>bytebufferpool.Get()</code>取得一个<code>bytebufferpoo.Buffer</code>对象去调用<code>ExecuteFunc()</code>办法。所以外围就是<code>ExecuteFunc()</code>办法:</p><pre><code class=“golang”>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}</code></pre><p>整个逻辑也很清晰,<code>for</code>循环就是<code>Write</code>一个<code>texts</code>元素,以以后的<code>tag</code>执行<code>TagFunc</code>,索引 +1。最初写入最初一个<code>texts</code>元素,实现。大略是这样:</p><pre><code class=“golang”>| text | tag | text | tag | text | … | tag | text |</code></pre><p>注:<code>ExecuteFuncStringWithErr()</code>办法应用到了后面文章介绍的<code>bytebufferpool</code>,感兴趣能够回去翻看。</p><h2>总结</h2><p>能够应用<code>fasttemplate</code>实现<code>strings.Replace</code>和<code>fmt.Sprintf</code>的工作,而且<code>fasttemplate</code>灵活性更高。代码清晰易懂,值得一看。</p><p>吐槽:对于命名,<code>Execute()</code>办法外面应用<code>stdTagFunc</code>,<code>ExecuteStd()</code>办法外面应用<code>keepUnknownTagFunc</code>办法。我想是不是把<code>stdTagFunc</code>改名为<code>defaultTagFunc</code>好一点?</p><p>大家如果发现好玩、好用的 Go 语言库,欢送到 Go 每日一库 GitHub 上提交 issue</p><h2>参考</h2><ol><li>fasttemplate GitHub:github.com/valyala/fasttemplate</li><li>Go 每日一库 GitHub:https://github.com/darjun/go-daily-lib</li></ol><h2>我</h2><p>我的博客:https://darjun.github.io</p><p>欢送关注我的微信公众号【GoUpUp】,独特学习,一起提高~</p></article>
...