Conditional statements

Continuing with our Person example, supposing we just want to print out the list of emails, without digging into it. We can do that with a template

  1. Name is {{.Name}}
  2. Emails are {{.Emails}}

This will print

  1. Name is jan
  2. Emails are [jan@newmarch.name jan.newmarch@gmail.com]

because that is how the fmt package will display a list.

In many circumstances that may be fine, if that is what you want. Let’s consider a case where it is almost right but not quite. There is a JSON package to serialise objects, which we looked at in Chapter 4. This would produce

  1. {"Name": "jan",
  2. "Emails": ["jan@newmarch.name", "jan.newmarch@gmail.com"]
  3. }

The JSON package is the one you would use in practice, but let’s see if we can produce JSON output using templates. We can do something similar just by the templates we have. This is almost right as a JSON serialiser:

  1. {"Name": "{{.Name}}",
  2. "Emails": {{.Emails}}
  3. }

It will produce

  1. {"Name": "jan",
  2. "Emails": [jan@newmarch.name jan.newmarch@gmail.com]
  3. }

which has two problems: the addresses aren’t in quotes, and the list elements should be ',' separated.

How about this: looking at the array elements, putting them in quotes and adding commas?

  1. {"Name": {{.Name}},
  2. "Emails": [
  3. {{range .Emails}}
  4. "{{.}}",
  5. {{end}}
  6. ]
  7. }

which will produce

  1. {"Name": "jan",
  2. "Emails": ["jan@newmarch.name", "jan.newmarch@gmail.com",]
  3. }

(plus some white space.).

Again, almost correct, but if you look carefully, you will see a trailing ',' after the last list element. According to the JSON syntax (see json.org, this trailing ',' is not allowed. Implementations may vary in how they deal with this.

What we want is “print every element followed by a ',' except for the last one. This is actually a bit hard to do, so a better way is “print every element preceded by a ',' except for the first one. (I got this tip from “brianb” at Stack Overflow.). This is easier, because the first element has index zero and many programming languages, including the Go template language, treat zero as Boolean false.

One form of the conditional statement is {{if pipeline}} T1 {{else}} T0 {{end}}. We need the pipeline to be the index into the array of emails. Fortunately, a variation on the range statement gives us this. There are two forms which introduce variables

  1. {{range $elmt := array}}
  2. {{range $index, $elmt := array}}

So we set up a loop through the array, and if the index is false (0) we just print the element, otherwise print it preceded by a ','. The template is

  1. {"Name": "{{.Name}}",
  2. "Emails": [
  3. {{range $index, $elmt := .Emails}}
  4. {{if $index}}
  5. , "{{$elmt}}"
  6. {{else}}
  7. "{{$elmt}}"
  8. {{end}}
  9. {{end}}
  10. ]
  11. }

and the full program is

  1. /**
  2. * PrintJSONEmails
  3. */
  4. package main
  5. import (
  6. "html/template"
  7. "os"
  8. "fmt"
  9. )
  10. type Person struct {
  11. Name string
  12. Emails []string
  13. }
  14. const templ = `{"Name": "{{.Name}}",
  15. "Emails": [
  16. {{range $index, $elmt := .Emails}}
  17. {{if $index}}
  18. , "{{$elmt}}"
  19. {{else}}
  20. "{{$elmt}}"
  21. {{end}}
  22. {{end}}
  23. ]
  24. }
  25. `
  26. func main() {
  27. person := Person{
  28. Name: "jan",
  29. Emails: []string{"jan@newmarch.name", "jan.newmarch@gmail.com"},
  30. }
  31. t := template.New("Person template")
  32. t, err := t.Parse(templ)
  33. checkError(err)
  34. err = t.Execute(os.Stdout, person)
  35. checkError(err)
  36. }
  37. func checkError(err error) {
  38. if err != nil {
  39. fmt.Println("Fatal error ", err.Error())
  40. os.Exit(1)
  41. }
  42. }

This gives the correct JSON output.

Before leaving this section, we note that the problem of formatting a list with comma separators can be approached by defining suitable functions in Go that are made available as template functions. To re-use a well known saying, “There’s more than one way to do it!”. The following program was sent to me by Roger Peppe:

  1. /**
  2. * Sequence.go
  3. * Copyright Roger Peppe
  4. */
  5. package main
  6. import (
  7. "errors"
  8. "fmt"
  9. "os"
  10. "text/template"
  11. )
  12. var tmpl = `{{$comma := sequence "" ", "}}
  13. {{range $}}{{$comma.Next}}{{.}}{{end}}
  14. {{$comma := sequence "" ", "}}
  15. {{$colour := cycle "black" "white" "red"}}
  16. {{range $}}{{$comma.Next}}{{.}} in {{$colour.Next}}{{end}}
  17. `
  18. var fmap = template.FuncMap{
  19. "sequence": sequenceFunc,
  20. "cycle": cycleFunc,
  21. }
  22. func main() {
  23. t, err := template.New("").Funcs(fmap).Parse(tmpl)
  24. if err != nil {
  25. fmt.Printf("parse error: %v\n", err)
  26. return
  27. }
  28. err = t.Execute(os.Stdout, []string{"a", "b", "c", "d", "e", "f"})
  29. if err != nil {
  30. fmt.Printf("exec error: %v\n", err)
  31. }
  32. }
  33. type generator struct {
  34. ss []string
  35. i int
  36. f func(s []string, i int) string
  37. }
  38. func (seq *generator) Next() string {
  39. s := seq.f(seq.ss, seq.i)
  40. seq.i++
  41. return s
  42. }
  43. func sequenceGen(ss []string, i int) string {
  44. if i >= len(ss) {
  45. return ss[len(ss)-1]
  46. }
  47. return ss[i]
  48. }
  49. func cycleGen(ss []string, i int) string {
  50. return ss[i%len(ss)]
  51. }
  52. func sequenceFunc(ss ...string) (*generator, error) {
  53. if len(ss) == 0 {
  54. return nil, errors.New("sequence must have at least one element")
  55. }
  56. return &generator{ss, 0, sequenceGen}, nil
  57. }
  58. func cycleFunc(ss ...string) (*generator, error) {
  59. if len(ss) == 0 {
  60. return nil, errors.New("cycle must have at least one element")
  61. }
  62. return &generator{ss, 0, cycleGen}, nil
  63. }

Conclusion

The Go template package is useful for certain kinds of text transformations involving inserting values of objects. It does not have the power of, say, regular expressions, but is faster and in many cases will be easier to use than regular expressions.