Go 模板


原文鏈接

很多語言都有很多方式將字符串從一只形式轉換成另一種形式。Go 使用模板的方法通過提供一個對象作為參數來轉換字符串。這個一般來講是用來將對象插入到HTML中的,不過它同樣可以用在其他的情況下。注意這部分跟網絡編程毫無關系,不過對於網絡編程來說很有用。

介紹

   大多數后端語言都能夠將動態生成的組件插入到靜態頁面中,例如一個list。典型的例子像JSP,PHP等等。Go 采用了一個相對來說簡單的腳本語言。
   重新編寫一個template包已經通過了。關於template包的文檔很少。現在還可以在old/template,中找到。現在在參考頁還沒有相關的包的文檔。模板的變化可以再r60 (released 2011/09/07)中找到。
   我們在這里描述下新包。這個包設計的目的是通過使用一個對象的值改變原始文本從而達到輸入一個文本輸出一個不同的文本的目的。跟JSP或者其他的不同,Go的模板並沒有限制必須使用HTML文件,這樣就最大程度的使用它。
   被稱作模板的原始文件會包含了沒有被改變的文本和可以改變文本的命令。命令由”{ {} }” 分隔,跟JSP的命令<%= … =%> 和PHP的命令<?php … ?>相似。

插入對象

   一個模板應用到Go的對象上。Go對象的字段可以插入到模板中,同時也可深入到對象的字段,查找子字段等。當前對象使用”.”代表,因此如果插入的值是一個字符串就可以直接是用{ {.} }來表示。template通過使用fmt來將對象轉換為字符串。
   需要使用前綴’.’來將當前對象的字段插入到模板中,例如下面這個對象類型:

type Person struct {
        Name      string
        Age       int
        Emails     []string
        Jobs       []*Job
}
  然后你通過以下代碼可以插入Age和Name:
The name is {{.Name}}.
The age is {{.Age}}.
我們可以通過 range命令來循環訪問數組或者列表中的元素,所有想要訪問Emails的信息可以使用如下代碼:
{{range .Emails}}
        ...
{{end}}
Job的定義如下:
type Job struct {
    Employer string
    Role     string
}
如果想要訪問Person的Jobs,我么可以使用上面的 { {range .Jobs} }。另一種方式是將Jobs字段變為當前字段。可以使用{ {with} } … { {end} }命令來實現,這樣{ {.} }表示的就是Jobs字段了。代碼如下:
{{with .Jobs}}
    {{range .}}
        An employer is {{.Employer}}
        and the role is {{.Role}}
    {{end}}
{{end}}

這個命令不僅僅可以用在數組中,它可以用在任何字段。
   當我們有了一個模板后,我們可以將其應用到一個對象中,通過將對象插入到模板中來生成新的字符串。包括解析模板,將模板應用到對象兩步處理。然后結果可以寫到Writer中輸出。例如:

t := template.New("Person template")
t, err := t.Parse(templ)
if err == nil {
    buff := bytes.NewBufferString("")
    t.Execute(buff, person)
}
下面是使用的完整的例子:
/**
 * PrintPerson
 */

package main

import (
    "fmt"
    "html/template"
    "os"
)

type Person struct {
    Name   string
    Age    int
    Emails []string
    Jobs   []*Job
}

type Job struct {
    Employer string
    Role     string
}

const templ = `The name is {{.Name}}.
The age is {{.Age}}.
{{range .Emails}}
        An email is {{.}}
{{end}}

{{with .Jobs}}
    {{range .}}
        An employer is {{.Employer}}
        and the role is {{.Role}}
    {{end}}
{{end}}
`

func main() {
    job1 := Job{Employer: "Monash", Role: "Honorary"}
    job2 := Job{Employer: "Box Hill", Role: "Head of HE"}

    person := Person{
        Name:   "jan",
        Age:    50,
        Emails: []string{"jan@newmarch.name", "jan.newmarch@gmail.com"},
        Jobs:   []*Job{&job1, &job2},
    }

    t := template.New("Person template")
    t, err := t.Parse(templ)
    checkError(err)

    err = t.Execute(os.Stdout, person)
    checkError(err)
}

func checkError(err error) {
    if err != nil {
        fmt.Println("Fatal error ", err.Error())
        os.Exit(1)
    }
}
 輸出結果是:
  
  
  
          
The name is jan.
The age is 50.

        An email is jan@newmarch.name

        An email is jan.newmarch@gmail.com



    
        An employer is Monash
        and the role is Honorary
    
        An employer is Box Hill
        and the role is Head of HE
注意這里有很多空格輸出,是因為這些空格在原始字符串就有,如果想要減少空格可以如下輸入:
{{range .Emails}} An email is {{.}} {{end}}
輸出結果是:

在這個例子中我們使用了一個字符串作為模板,我們同樣可以使用template.ParseFiles()方法從一個文件中獲取模板。

管道

   上面的轉換摻入了一些文本到模板中。這些文本基本上都是隨意的,不管這些文本是什么。如果我們想要將他們插入到HTML文件或者其他形式的文件中,這樣我們就需要對一些字符進行轉義。例如,為了再HTML中顯示任意文本,我們不得不講”<” 轉換為”&lt”。Go 的模板有很多內建的函數,其中一個就是’html’,這個函數跟unix的管道很想,從標准輸入中讀取然后寫到標准輸出。
   取當前對象的值並將其轉義輸出到HTML中,如下:

{{ . | html }}

其他的函數使用方法相同。

定義函數

   模板通過使用一個對象插入相關值,通過使用fmt將對象轉為字符串。有時候這並不是我們想要的。例如為了防止垃圾郵件發送的人得到你的郵箱,就需要將”@”轉為“at”。例如“jane at newmarch.name”。如果我們想要template這樣輸出,我們可以編寫一個轉換函數。
   每一個模板都有一個名字供自己使用,同時可以關聯一個Go的函數。這個通過下面這個類型關聯:

type FuncMap map[string]interface{}
例如,如果我們想讓我們的函數 EmailExpanderemailExpand關聯,我么可以在template中加入這個語句:
t = t.Funcs(template.FuncMap("emailExpand", EmailExpander))
EmailExpander的簽名如下:
func EmailExpander(args ...interface{}) string
我們使用這個函數只是對傳入的字符串類型感興趣。Go 模板的代碼已經有一些初始化的代碼,所以我們直接復制這些代碼來用。然后做了一些小修改,一些是完整代碼:
/**
 * PrintEmails
 */

package main

import (
    "fmt"
    "os"
    "strings"
    "text/template"
)

type Person struct {
    Name   string
    Emails []string
}

const templ = `The name is {{.Name}}.
{{range .Emails}}
        An email is "{{. | emailExpand}}"
{{end}}
`

func EmailExpander(args ...interface{}) string {
    ok := false
    var s string
    if len(args) == 1 {
        s, ok = args[0].(string)
    }
    if !ok {
        s = fmt.Sprint(args...)
    }

    // find the @ symbol
    substrs := strings.Split(s, "@")
    if len(substrs) != 2 {
        return s
    }
    // replace the @ by " at "
    return (substrs[0] + " at " + substrs[1])
}

func main() {
    person := Person{
        Name:   "jan",
        Emails: []string{"jan@newmarch.name", "jan.newmarch@gmail.com"},
    }

    t := template.New("Person template")

    // add our function
    t = t.Funcs(template.FuncMap{"emailExpand": EmailExpander})

    t, err := t.Parse(templ)

    checkError(err)

    err = t.Execute(os.Stdout, person)
    checkError(err)
}

func checkError(err error) {
    if err != nil {
        fmt.Println("Fatal error ", err.Error())
        os.Exit(1)
    }
}
輸出是:
The name is jan.

        An email is "jan at newmarch.name"

        An email is "jan.newmarch at gmail.com"
變量

   模板包允許我們定義並使用變量。為了這個目的,我們可以在每個email的輸出前加一個名字作為前綴,入下:

type Person struct {
        Name      string
        Emails     []string
}
為了訪問email,我們可以使用range命令。
{{range .Emails}}
    {{.}}
{{end}}
但是現在不能通過 . 來訪問Name字段了,因為現在Name已經不在范圍之內。解決方法就是將其出入一個變量以供訪問。通過前面加“$”來定義變量:
{{$name := .Name}}
{{range .Emails}}
    Name is {{$name}}, email is {{.}}
{{end}}
程序如下:
/**
 * PrintNameEmails
 */

package main

import (
    "html/template"
    "os"
    "fmt"
)

type Person struct {
    Name   string
    Emails []string
}

const templ = `{{$name := .Name}}
{{range .Emails}}
    Name is {{$name}}, email is {{.}}
{{end}}
`

func main() {
    person := Person{
        Name:   "jan",
        Emails: []string{"jan@newmarch.name", "jan.newmarch@gmail.com"},
    }

    t := template.New("Person template")
    t, err := t.Parse(templ)
    checkError(err)

    err = t.Execute(os.Stdout, person)
    checkError(err)
}

func checkError(err error) {
    if err != nil {
        fmt.Println("Fatal error ", err.Error())
        os.Exit(1)
    }
}
輸出:
Name is jan, email is jan@newmarch.name

Name is jan, email is jan.newmarch@gmail.com
條件語句

   繼續我們Person的例子,假設我們想要輸出emails的列表而不是深入這個字段,那么我們可以這么寫模板:

Name is {{.Name}}
Emails are {{.Emails}}
它的輸出是:
Name is jan
Emails are [jan@newmarch.name jan.newmarch@gmail.com]

這個就是fmt輸出的格式。
   如果這是你想要的記過,那么在大多數情況下這樣輸出是沒有問題的。讓我們考慮一下那里差不多。有個JSON的包用來序列化對象,這個我們在第四章 講過。他的輸出方式為:

{"Name": "jan",
 "Emails": ["jan@newmarch.name", "jan.newmarch@gmail.com"]
}
這個是我們在JSON的練習,現在讓我們考慮下如何使用模板來輸出這樣的形式,下面這個代碼就差不多可以:
{"Name": "{{.Name}}",
 "Emails": {{.Emails}}
}
輸出為:
{"Name": "jan",
 "Emails": [jan@newmarch.name jan.newmarch@gmail.com]
}

這個還有兩個問題,郵箱地址沒有被引號括起來,列表沒有用逗號隔開。
   如果用下面的形式怎么樣?

{"Name": {{.Name}},
  "Emails": [
   {{range .Emails}}
      "{{.}}",
   {{end}}
  ]
}
輸出是:
{"Name": "jan",
 "Emails": ["jan@newmarch.name", "jan.newmarch@gmail.com",]
}

差不多正確了,不過仔細看。你會發現在列表末尾的元素有個逗號,根據JSON規范,是不允許這樣實現的。

   我們可以通過if語句來搞定這個問題,如下:

{"Name": "{{.Name}}",
 "Emails": [
 {{range $index, $elmt := .Emails}}
    {{if $index}}
        , "{{$elmt}}"
    {{else}}
         "{{$elmt}}"
    {{end}}
 {{end}}
 ]
}

這個程序如下:

/**
 * PrintJSONEmails
 */

package main

import (
    "html/template"
    "os"
    "fmt"
)

type Person struct {
    Name   string
    Emails []string
}

const templ = `{"Name": "{{.Name}}",
 "Emails": [
{{range $index, $elmt := .Emails}}
    {{if $index}}
        , "{{$elmt}}"
    {{else}}
         "{{$elmt}}"
    {{end}}
{{end}}
 ]
}
`

func main() {
    person := Person{
        Name:   "jan",
        Emails: []string{"jan@newmarch.name", "jan.newmarch@gmail.com"},
    }

    t := template.New("Person template")
    t, err := t.Parse(templ)
    checkError(err)

    err = t.Execute(os.Stdout, person)
    checkError(err)
}

func checkError(err error) {
    if err != nil {
        fmt.Println("Fatal error ", err.Error())
        os.Exit(1)
    }
}

  這個就會是一個正確的輸出。

   在結束本章之前,我么發現處理逗號可以通過定義一個Go的函數來實現。為了重用我們可以這樣寫:

/**
 * Sequence.go
 * Copyright Roger Peppe
 */

package main

import (
    "errors"
    "fmt"
    "os"
    "text/template"
)

var tmpl = `{{$comma := sequence "" ", "}}
{{range $}}{{$comma.Next}}{{.}}{{end}}
{{$comma := sequence "" ", "}}
{{$colour := cycle "black" "white" "red"}}
{{range $}}{{$comma.Next}}{{.}} in {{$colour.Next}}{{end}}
`

var fmap = template.FuncMap{
    "sequence": sequenceFunc,
    "cycle":    cycleFunc,
}

func main() {
    t, err := template.New("").Funcs(fmap).Parse(tmpl)
    if err != nil {
        fmt.Printf("parse error: %v\n", err)
        return
    }
    err = t.Execute(os.Stdout, []string{"a", "b", "c", "d", "e", "f"})
    if err != nil {
        fmt.Printf("exec error: %v\n", err)
    }
}

type generator struct {
    ss []string
    i  int
    f  func(s []string, i int) string
}

func (seq *generator) Next() string {
    s := seq.f(seq.ss, seq.i)
    seq.i++
    return s
}

func sequenceGen(ss []string, i int) string {
    if i >= len(ss) {
        return ss[len(ss)-1]
    }
    return ss[i]
}

func cycleGen(ss []string, i int) string {
    return ss[i%len(ss)]
}

func sequenceFunc(ss ...string) (*generator, error) {
    if len(ss) == 0 {
        return nil, errors.New("sequence must have at least one element")
    }
    return &generator{ss, 0, sequenceGen}, nil
}

func cycleFunc(ss ...string) (*generator, error) {
    if len(ss) == 0 {
        return nil, errors.New("cycle must have at least one element")
    }
    return &generator{ss, 0, cycleGen}, nil
}
結論

   Go的模板對於一些將對象插入到模板的文本轉換很有幫助。他沒有使用功能強大的正則表達式,但是相比正則表達式它更快更易用。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM