fasttemplate is a relatively simple and easy to use small template library . The author of fasttemplate, valyala, has additionally open sourced a number of excellent libraries, such as the famous fasthttp.

fasttemlate only focuses on a very small area - string replacement. Its goal is to replace strings.Replace, fmt.Sprintf and other methods to provide a simple, easy-to-use, high-performance string replacement method.

Quick Use

Create the directory and initialize.

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

Install the fasttemplate library.

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

Write the code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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)
}
  • Define the template string, using {{ and }} for placeholders, which can be specified when creating the template
  • Call fasttemplate.New() to create a template object t, passing in the start and end placeholders
  • Call the t.ExecuteString() method of the template object, passing in the parameters. The parameters have the values corresponding to each placeholder. Generate the final string

Running results:

1
2
name: dj
age: 18

We can customize the placeholders, the above uses {{ and }} as start and end placeholders respectively. We can replace them with [[ and ]] with a simple code change

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

Also, note that the incoming parameters are of type map[string]interface{}, but fasttemplate only accepts values of type []byte, string and TagFunc. This is why the 18 above is enclosed in double quotes.

Another point to note is that fasttemplate.New() returns a template object, and if the template fails to parse, it will just panic. If you want to handle the error yourself, you can call the fasttemplate.NewTemplate() method, which returns a template object and an error.

In fact, fasttemplate.New() internally calls fasttemplate.NewTemplate(), and if an error is returned, it will panic

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// 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
}

This is actually a common practice, for example, do not want to deal with the error of the program, direct panic is sometimes an option. For example, the html.template standard library also provides the Must() method, which is generally used to panic when a parsing failure is encountered.

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

No spaces inside the middle of placeholders!!!

Shortcuts

By using fasttemplate.New() to define a template object, we can use different parameters to do the replacement multiple times. However, sometimes we have to do a lot of one-time replacements, and defining the template object each time seems tedious. fasttemplate also provides a one-time replacement method.

1
2
3
4
5
6
7
8
9
func main() {
  template := `name: [name]
age: [age]`
  s := fasttemplate.ExecuteString(template, "[", "]", map[string]interface{}{
    "name": "dj",
    "age":  "18",
  })
  fmt.Println(s)
}

Using this approach, we need to pass in both the template string, the start placeholder, the end placeholder and the substitution parameter.

TagFunc

fasttemplate provides a TagFunc that adds some logic to the substitution. TagFunc is a function

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

When performing the replacement, fasttemplate calls the TagFuncfunction once for each placeholder, tag being the name of the placeholder. See the following program:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
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)
}

This is actually the TagFunc version of the get-started sample program, which writes different values depending on the tag passed in. If we look at the source code we can see that ExecuteString() actually ends up calling ExecuteFuncString(). The fasttemplate provides a standard TagFunc

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
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))
  }
}

The standard TagFunc implementation is also very simple, that is, the corresponding value is taken from the parameter map[string]interface{} and processed accordingly.

If it is []byte and string type, directly call the write method of io.Writer. If it is of type TagFunc, call this method directly, passing in io.Writer and tag. Other types throw errors directly with panic.

If the tag in the template does not exist in the parameter map[string]interface{}, there are two ways to deal with it:

  • is simply ignored, which is equivalent to replacing it with the empty string “”. This is how the standard stdTagFunc handles this.
  • Keep the original tag. keepUnknownTagFunc does just that.

The keepUnknownTagFunc code is as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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))
  }
}

The second half is handled the same as stdTagFunc, the first half of the function if tag is not found. Write startTag + tag + endTag directly as the replacement value.

The ExecuteString() method we called earlier uses stdTagFunc, i.e. it directly replaces the unrecognized tag with the empty string. If you want to keep the unrecognized tag, just call the ExecuteStringStd() method instead. This method will retain the unrecognized tag when it encounters it

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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)
}

The missing age in the parameters results in the following run

1
2
3
4
name: dj
age:
name: dj
age: {{age}}

Methods with io.Writer arguments

The previously described methods all return a string at the end. The method names all have String: ExecuteString()/ExecuteFuncString() in them.

We can pass a io.Writer parameter directly and call the Write() method of this parameter to write the result string directly. This class does not have String:Execute()/ExecuteFunc() in the method name.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
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
  })
}

Since os.Stdout implements the io.Writer interface, it can be passed in directly. The result is written directly to os.Stdout. Run.

1
2
3
4
name: dj
age: 18
name: hjw
age: 20

Source Code Analysis

First look at the structure and creation of the template object

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// 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
}

After the template is created the Reset() method is called to initialize.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
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
}

The initialization does the following:

  • Record start and end placeholders.
  • Parse the template to separate the text and tag slices and store them in the texts and tags slices respectively. The second half of the for loop does just that.

Code detail points:

  • First count the total number of placeholders, construct the text and tag slices of the corresponding size at a time, and note that a properly constructed template string text slice must be 1 larger than the tag slice. like this | text | tag | text | ... | tag | text |.
  • To avoid memory copies, use unsafeString2Bytes to let the returned byte slices point directly to the string’s internal address.

Looking at the above introduction, it seems that there are many methods. In fact the core method is just one ExecuteFunc(). All other methods call it directly or indirectly.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 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) })
}

The Execute() method constructs a TagFunc to call ExecuteFunc(), internally using stdTagFunc.

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

The ExecuteString() and ExecuteStringStd() methods call the ExecuteFuncString() method, which in turn calls the ExecuteFuncStringWithErr() method,

ExecuteFuncStringWithErr() method internally uses bytebufferpool.Get() to get a bytebufferpoo.Buffer object to call the ExecuteFunc() method. So the core is the ExecuteFunc() method

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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
}

The whole logic is also very clear, for loop is Write a texts element, to the current tag to execute TagFunc, index +1. Finally write the last texts element, complete. It looks like this.

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

Summary

You can use fasttemplate to accomplish the tasks of strings.Replace and fmt.Sprintf, and fasttemplate is more flexible. The code is clear and easy to understand, worth a look.

Regarding the naming, the Execute() method uses stdTagFunc inside and the ExecuteStd() method uses the keepUnknownTagFunc method inside. I wonder if it would be better to rename stdTagFunc to defaultTagFunc?


Reference https://darjun.github.io/2021/05/24/godailylib/fasttemplate/