<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Go 每日一庫(kù)之 fasttemplate

          共 17071字,需瀏覽 35分鐘

           ·

          2021-06-03 00:15

          簡(jiǎn)介

          fasttemplate是一個(gè)比較簡(jiǎn)單、易用的小型模板庫(kù)。fasttemplate的作者valyala另外還開(kāi)源了不少優(yōu)秀的庫(kù),如大名鼎鼎的fasthttp,前面介紹的bytebufferpool,還有一個(gè)重量級(jí)的模板庫(kù)quicktemplatequicktemplate比標(biāo)準(zhǔn)庫(kù)中的text/templatehtml/template要靈活和易用很多,后面會(huì)專(zhuān)門(mén)介紹它。今天要介紹的fasttemlate只專(zhuān)注于一塊很小的領(lǐng)域——字符串替換。它的目標(biāo)是為了替代strings.Replacefmt.Sprintf等方法,提供一個(gè)簡(jiǎn)單,易用,高性能的字符串替換方法。

          本文首先介紹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庫(kù):

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

          編寫(xiě)代碼:

          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,傳入開(kāi)始和結(jié)束占位符;
          • 調(diào)用模板對(duì)象的t.ExecuteString()方法,傳入?yún)?shù)。參數(shù)中有各個(gè)占位符對(duì)應(yīng)的值。生成最終的字符串。

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

          name: dj
          age: 18

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

          template := `name: [[name]]
          age: [[age]]`

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

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

          另一個(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)庫(kù)也提供了Must()方法,一般這樣用,遇到解析失敗就panic

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

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

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

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

          快捷方式

          使用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í)傳入模板字符串、開(kāi)始占位符、結(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即占位符的名稱(chēng)。看下面程序:

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

            fmt.Println(s)
          }

          這其實(shí)就是get-started示例程序的TagFunc版本,根據(jù)傳入的tag寫(xiě)入不同的值。如果我們?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 0nil
            }
            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)也非常簡(jiǎn)單,就是從參數(shù)map[string]interface{}中取出對(duì)應(yīng)的值做相應(yīng)處理,如果是[]bytestring類(lèi)型,直接調(diào)用io.Writer的寫(xiě)入方法。如果是TagFunc類(lèi)型則直接調(diào)用該方法,將io.Writertag傳入。其他類(lèi)型直接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 0nil
            }
            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未找到。直接寫(xiě)入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()方法直接寫(xiě)入。這類(lèi)方法名中沒(méi)有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 0nil
            })
          }

          由于os.Stdout實(shí)現(xiàn)了io.Writer接口,可以直接傳入。結(jié)果直接寫(xiě)到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([][]byte0, tagsCount+1)
            }
            if tagsCount > cap(t.tags) {
              t.tags = make([]string0, 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
          }

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

          • 記錄開(kāi)始和結(jié)束占位符;
          • 解析模板,將文本和tag切分開(kāi),分別存放在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。最后寫(xiě)入最后一個(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 語(yǔ)言庫(kù),歡迎到 Go 每日一庫(kù) GitHub 上提交 issue??

          參考

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


          推薦閱讀


          福利

          我為大家整理了一份從入門(mén)到進(jìn)階的Go學(xué)習(xí)資料禮包,包含學(xué)習(xí)建議:入門(mén)看什么,進(jìn)階看什么。關(guān)注公眾號(hào) 「polarisxu」,回復(fù) ebook 獲取;還可以回復(fù)「進(jìn)群」,和數(shù)萬(wàn) Gopher 交流學(xué)習(xí)。

          瀏覽 20
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  艹屄视频在线观看 | 国内极品视频在线观看 | 欧美日韩第一区 | 久久急费看黄A毛片 | 激情无码国产 |