Go 每日一庫之 fasttemplate

簡介

fasttemplate是一個(gè)比較簡單虹蒋、易用的小型模板庫椭赋。fasttemplate的作者valyala另外還開源了不少優(yōu)秀的庫功舀,如大名鼎鼎的fasthttp令漂,前面介紹的bytebufferpool挂据,還有一個(gè)重量級(jí)的模板庫quicktemplate胖喳。quicktemplate比標(biāo)準(zhǔn)庫中的text/templatehtml/template要靈活和易用很多泡躯,后面會(huì)專門介紹它。今天要介紹的fasttemlate只專注于一塊很小的領(lǐng)域——字符串替換丽焊。它的目標(biāo)是為了替代strings.Replace较剃、fmt.Sprintf等方法,提供一個(gè)簡單技健,易用写穴,高性能的字符串替換方法。

本文首先介紹fasttemplate的用法雌贱,然后去看看源碼實(shí)現(xiàn)的一些細(xì)節(jié)啊送。

快速使用

本文代碼使用 Go Modules。

創(chuàng)建目錄并初始化:

$ 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)
}
  • 定義模板字符串欣孤,使用{{}}表示占位符馋没,占位符可以在創(chuàng)建模板的時(shí)候指定;
  • 調(diào)用fasttemplate.New()創(chuàng)建一個(gè)模板對(duì)象t降传,傳入開始和結(jié)束占位符篷朵;
  • 調(diào)用模板對(duì)象的t.ExecuteString()方法,傳入?yún)?shù)婆排。參數(shù)中有各個(gè)占位符對(duì)應(yīng)的值声旺。生成最終的字符串。

運(yùn)行結(jié)果:

name: dj
age: 18

我們可以自定義占位符泽论,上面分別使用{{}}作為開始和結(jié)束占位符艾少。我們可以換成[[]],只需要簡單修改一下代碼即可:

template := `name: [[name]]
age: [[age]]`
t := fasttemplate.New(template, "[[", "]]")

另外翼悴,需要注意的是缚够,傳入?yún)?shù)的類型為map[string]interface{}幔妨,但是fasttemplate只接受類型為[]bytestringTagFunc類型的值谍椅。這也是為什么上面的18要用雙引號(hào)括起來的原因误堡。

另一個(gè)需要注意的點(diǎn),fasttemplate.New()返回一個(gè)模板對(duì)象雏吭,如果模板解析失敗了锁施,就會(huì)直接panic。如果想要自己處理錯(cuò)誤杖们,可以調(diào)用fasttemplate.NewTemplate()方法悉抵,該方法返回一個(gè)模板對(duì)象和一個(gè)錯(cuò)誤。實(shí)際上摘完,fasttemplate.New()內(nèi)部就是調(diào)用fasttemplate.NewTemplate()姥饰,如果返回了錯(cuò)誤,就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
}

這其實(shí)也是一種慣用法孝治,對(duì)于不想處理錯(cuò)誤的示例程序列粪,直接panic有時(shí)也是一種選擇。例如html.template標(biāo)準(zhǔn)庫也提供了Must()方法谈飒,一般這樣用岂座,遇到解析失敗就panic

t := template.Must(template.New("name").Parse("html"))

占位符中間內(nèi)部不要加空格!:即搿费什!

占位符中間內(nèi)部不要加空格!H拷椤吕喘!

占位符中間內(nèi)部不要加空格!P躺!氯质!

快捷方式

使用fasttemplate.New()定義模板對(duì)象的方式,我們可以多次使用不同的參數(shù)去做替換祠斧。但是闻察,有時(shí)候我們要做大量一次性的替換,每次都定義模板對(duì)象顯得比較繁瑣琢锋。fasttemplate也提供了一次性替換的方法:

func main() {
  template := `name: [name]
age: [age]`
  s := fasttemplate.ExecuteString(template, "[", "]", map[string]interface{}{
    "name": "dj",
    "age":  "18",
  })
  fmt.Println(s)
}

使用這種方式辕漂,我們需要同時(shí)傳入模板字符串、開始占位符吴超、結(jié)束占位符和替換參數(shù)钉嘹。

TagFunc

fasttemplate提供了一個(gè)TagFunc,可以給替換增加一些邏輯鲸阻。TagFunc是一個(gè)函數(shù):

type TagFunc func(w io.Writer, tag string) (int, error)

在執(zhí)行替換的時(shí)候跋涣,fasttemplate針對(duì)每個(gè)占位符都會(huì)調(diào)用一次TagFunc函數(shù)缨睡,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)
}

這其實(shí)就是get-started示例程序的TagFunc版本奖年,根據(jù)傳入的tag寫入不同的值。如果我們?nèi)ゲ榭丛创a就會(huì)發(fā)現(xiàn)沛贪,實(shí)際上ExecuteString()最終還是會(huì)調(diào)用ExecuteFuncString()陋守。fasttemplate提供了一個(gè)標(biāo)準(zhǔn)的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))
  }
}

標(biāo)準(zhǔn)的TagFunc實(shí)現(xiàn)也非常簡單,就是從參數(shù)map[string]interface{}中取出對(duì)應(yīng)的值做相應(yīng)處理利赋,如果是[]bytestring類型水评,直接調(diào)用io.Writer的寫入方法。如果是TagFunc類型則直接調(diào)用該方法隐砸,將io.Writertag傳入之碗。其他類型直接panic拋出錯(cuò)誤蝙眶。

如果模板中的tag在參數(shù)map[string]interface{}中不存在季希,有兩種處理方式:

  • 直接忽略,相當(dāng)于替換成了空字符串""幽纷。標(biāo)準(zhǔn)的stdTagFunc就是這樣處理的式塌;
  • 保留原始tagkeepUnknownTagFunc就是做這個(gè)事情的友浸。

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一樣峰尝,函數(shù)前半部分如果tag未找到。直接寫入startTag + tag + endTag作為替換的值收恢。

我們前面調(diào)用的ExecuteString()方法使用stdTagFunc武学,即直接將未識(shí)別的tag替換成空字符串。如果想保留未識(shí)別的tag伦意,改為調(diào)用ExecuteStringStd()方法即可火窒。該方法遇到未識(shí)別的tag會(huì)保留:

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)
}

參數(shù)中缺少age,運(yùn)行結(jié)果:

name: dj
age:
name: dj
age: {{age}}

io.Writer參數(shù)的方法

前面介紹的方法最后都是返回一個(gè)字符串驮肉。方法名中都有StringExecuteString()/ExecuteFuncString()熏矿。

我們可以直接傳入一個(gè)io.Writer參數(shù),將結(jié)果字符串調(diào)用這個(gè)參數(shù)的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實(shí)現(xiàn)了io.Writer接口票编,可以直接傳入。結(jié)果直接寫到os.Stdout中卵渴。運(yùn)行:

name: dj
age: 18
name: hjw
age: 20

源碼分析

首先看模板對(duì)象的結(jié)構(gòu)和創(chuàng)建:

// 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
}

模板創(chuàng)建之后會(huì)調(diào)用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
}

初始化做了下面這些事情:

  • 記錄開始和結(jié)束占位符慧域;
  • 解析模板,將文本和tag切分開浪读,分別存放在textstags切片中昔榴。后半段的for循環(huán)就是做的這個(gè)事情宛裕。

代碼細(xì)節(jié)點(diǎn):

  • 先統(tǒng)計(jì)占位符一共多少個(gè),一次構(gòu)造對(duì)應(yīng)大小的文本和tag切片论泛,注意構(gòu)造正確的模板字符串文本切片一定比tag切片大 1揩尸。像這樣| text | tag | text | ... | tag | text |
  • 為了避免內(nèi)存拷貝屁奏,使用unsafeString2Bytes讓返回的字節(jié)切片直接指向string內(nèi)部地址岩榆。

看上面的介紹,貌似有很多方法坟瓢。實(shí)際上核心的方法就一個(gè)ExecuteFunc()勇边。其他的方法都是直接或間接地調(diào)用它:

// 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()方法構(gòu)造一個(gè)TagFunc調(diào)用ExecuteFunc(),內(nèi)部使用stdTagFunc

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

ExecuteStd()方法構(gòu)造一個(gè)TagFunc調(diào)用ExecuteFunc()折联,內(nèi)部使用keepUnknownTagFunc

func(w io.Writer, tag string) (int, error) {
  return keepUnknownTagFunc(w, t.startTag, t.endTag, tag, m)
}

ExecuteString()ExecuteStringStd()方法調(diào)用ExecuteFuncString()方法粒褒,而ExecuteFuncString()方法又調(diào)用了ExecuteFuncStringWithErr()方法,ExecuteFuncStringWithErr()方法內(nèi)部使用bytebufferpool.Get()獲得一個(gè)bytebufferpoo.Buffer對(duì)象去調(diào)用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
}

整個(gè)邏輯也很清晰奕坟,for循環(huán)就是Write一個(gè)texts元素,以當(dāng)前的tag執(zhí)行TagFunc清笨,索引 +1月杉。最后寫入最后一個(gè)texts元素,完成抠艾。大概是這樣:

| text | tag | text | tag | text | ... | tag | text |

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

總結(jié)

可以使用fasttemplate完成strings.Replacefmt.Sprintf的任務(wù)检号,而且fasttemplate靈活性更高腌歉。代碼清晰易懂,值得一看齐苛。

吐槽:關(guān)于命名翘盖,Execute()方法里面使用stdTagFuncExecuteStd()方法里面使用keepUnknownTagFunc方法脸狸。我想是不是把stdTagFunc改名為defaultTagFunc好一點(diǎn)最仑?

大家如果發(fā)現(xiàn)好玩、好用的 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

歡迎關(guān)注我的微信公眾號(hào)【GoUpUp】泥彤,共同學(xué)習(xí),一起進(jìn)步~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末卿啡,一起剝皮案震驚了整個(gè)濱河市吟吝,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌颈娜,老刑警劉巖剑逃,帶你破解...
    沈念sama閱讀 206,723評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件浙宜,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡蛹磺,警方通過查閱死者的電腦和手機(jī)粟瞬,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來萤捆,“玉大人裙品,你說我怎么就攤上這事∷谆颍” “怎么了市怎?”我有些...
    開封第一講書人閱讀 152,998評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長辛慰。 經(jīng)常有香客問我区匠,道長,這世上最難降的妖魔是什么帅腌? 我笑而不...
    開封第一講書人閱讀 55,323評(píng)論 1 279
  • 正文 為了忘掉前任驰弄,我火速辦了婚禮,結(jié)果婚禮上狞膘,老公的妹妹穿的比我還像新娘揩懒。我一直安慰自己,他們只是感情好挽封,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,355評(píng)論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著臣镣,像睡著了一般辅愿。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上忆某,一...
    開封第一講書人閱讀 49,079評(píng)論 1 285
  • 那天点待,我揣著相機(jī)與錄音,去河邊找鬼弃舒。 笑死癞埠,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的聋呢。 我是一名探鬼主播苗踪,決...
    沈念sama閱讀 38,389評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼削锰!你這毒婦竟也來了通铲?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,019評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤器贩,失蹤者是張志新(化名)和其女友劉穎颅夺,沒想到半個(gè)月后朋截,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,519評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡吧黄,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,971評(píng)論 2 325
  • 正文 我和宋清朗相戀三年嵌灰,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了涂乌。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,100評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖亡哄,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情盼铁,我是刑警寧澤醋粟,帶...
    沈念sama閱讀 33,738評(píng)論 4 324
  • 正文 年R本政府宣布,位于F島的核電站昌讲,受9級(jí)特大地震影響国夜,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜短绸,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,293評(píng)論 3 307
  • 文/蒙蒙 一车吹、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧醋闭,春花似錦窄驹、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,289評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至囚企,卻和暖如春丈咐,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背龙宏。 一陣腳步聲響...
    開封第一講書人閱讀 31,517評(píng)論 1 262
  • 我被黑心中介騙來泰國打工棵逊, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人银酗。 一個(gè)月前我還...
    沈念sama閱讀 45,547評(píng)論 2 354
  • 正文 我出身青樓辆影,卻偏偏與公主長得像,于是被迫代替她去往敵國和親黍特。 傳聞我的和親對(duì)象是個(gè)殘疾皇子蛙讥,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,834評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容