乐趣区

关于godailylib:Go-每日一库之-fasttemplate

简介

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

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

疾速应用

本文代码应用 Go Modules。

创立目录并初始化:

$ 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)
}
  • 定义模板字符串,应用 {{}}示意占位符,占位符能够在创立模板的时候指定;
  • 调用 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】,独特学习,一起提高~

退出移动版