Go 每日一庫之 fasttemplate

簡介

fasttemplate是一個比較簡單、易用的小型模板庫。fasttemplate的做者valyala另外還開源了很多優秀的庫,如大名鼎鼎的fasthttp,前面介紹的bytebufferpool,還有一個重量級的模板庫quicktemplatequicktemplate比標準庫中的text/templatehtml/template要靈活和易用不少,後面會專門介紹它。今天要介紹的fasttemlate只專一於一塊很小的領域——字符串替換。它的目標是爲了替代strings.Replacefmt.Sprintf等方法,提供一個簡單,易用,高性能的字符串替換方法。html

本文首先介紹fasttemplate的用法,而後去看看源碼實現的一些細節。git

快速使用

本文代碼使用 Go Modules。github

建立目錄並初始化:golang

$ mkdir fasttemplate && cd fasttemplate
$ go mod init github.com/darjun/go-daily-lib/fasttemplate

安裝fasttemplate庫:微信

$ go get -u github.com/valyala/fasttemplate

編寫代碼:app

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😄

參考

  1. fasttemplate GitHub:github.com/valyala/fasttemplate
  2. Go 每日一庫 GitHub:https://github.com/darjun/go-daily-lib

個人博客:https://darjun.github.io

歡迎關注個人微信公衆號【GoUpUp】,共同窗習,一塊兒進步~

相關文章
相關標籤/搜索