天天看點

go template文法:解析和建立模闆,模闆變量,模闆動作,模闆函數,模闆比較函數,嵌套模闆和布局,模闆調用函數

Go标準庫提供了幾個package可以産生輸出結果,而​​text/template ​​​提供了基于模闆輸出文本内容的功能。​​html/template​​則是産生 安全的HTML格式的輸出。這兩個包使用相同的接口,但是我下面的例子主要面向HTML應用。

解析和建立模闆

命名模闆

模闆沒有限定擴充名,最流行的字尾是​

​.tmpl​

​​, vim-go提供了對它的支援,并且godoc的例子中也使用這個​​字尾​​​。Atom 和 GoSublime 對​

​.gohtml​

​​字尾的檔案提供了文法高亮的支援。通過對代碼庫的分析統計發現​

​.tpl​

​字尾也被經常使用。當然字尾并不重要,在項目中保持清晰和一緻即可。

建立模闆

​tpl, err := template.Parse(filename)​

​​得到檔案名為名字的模闆,并儲存在​

​tpl​

​變量中。tpl可以被執行來顯示模闆。

解析多個模闆

​template.ParseFiles(filenames)​

​​可以解析一組模闆,使用檔案名作為模闆的名字。​

​template.ParseGlob(pattern)​

​​會根據​

​pattern​

​解析所有比對的模闆并儲存。

解析字元串模闆

​t, err := template.New("foo").Parse(\​

​​{ {define “T”}}Hello, { {.}}!{ {end}}​

​)​

​ 可以解析字元串模闆,并設定它的名字。

執行模闆

執行簡單模闆

又兩種方式執行模闆。簡單的模闆​

​tpl​

​​可以通過​

​tpl.Execute(io.Writer, data)​

​​去執行, 模闆渲染後的内容寫入到​

​io.Writer​

​​中。​

​Data​

​是傳給模闆的動态資料。

執行命名的模闆

​tpl.ExecuteTemplate(io.Writer, name, data)​

​​和上面的簡單模闆類似,隻不過傳入了一個模闆的名字,指定要渲染的模闆(因為​

​tpl​

​可以包含多個模闆)。

模闆編碼和HTML

上下文編碼

​html/template​

​基于上下文資訊進行編碼,是以任何需要編碼的字元都能被正确的進行編碼。

例如​

​"<h1>A header!</h1>"​

​​中的尖括号會被編碼為​

​&lt;h1&gt;A header!&lt;/h1&gt;​

​。

​template.HTML​

​​可以告訴Go要處理的字元串是安全的,不需要編碼。​

​template.HTML("<h1>A Safe header</h1>")​

​​會輸出​

​<h1>A Safe header</h1>​

​,注意這個方法處理使用者的輸入的時候比較危險。

​html/template​

​還可以根據模闆中的屬性進行不同的編碼。(The go html/template package is aware of attributes within the template and will encode values differently based on the attribute.)

Go 模闆也可以應用javascript。struct和map被展開為JSON 對象,引号會被增加到字元串中,,用做函數參數和變量的值。

// Go
type Cat struct {
  Name string
  Age int
}

kitten := Cat{"Sam", 12}

// Template
<script>
  var cat = { {.kitten}}
</script>

// Javascript
var cat = {"Name":"Sam", "Age" 12}      

安全字元串和 HTML注釋

預設情況下 ​

​html/template​

​會删除模闆中的所有注釋,這會導緻一些問題,因為有些注釋是有用的,比如:

<!--[if IE]>
Place content here to target all Internet Explorer users.
<![endif]-->      

我們可以使用自定義的方法建立一個可以傳回注釋的函數。在FuncMap中定義​

​htmlSafe​

​方法:

testTemplate, err = template.New("hello.gohtml").Funcs(template.FuncMap{
    "htmlSafe": func(html string) template.HTML {
        return template.HTML(html)
    },
}).ParseFiles("hello.gohtml")      

這個函數會産生一模一樣的HTML代碼,這個函數可以用在模闆中保留前面的注釋:

{ {htmlSafe "<!--[if IE 6]>" }}
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">  
{ { htmlSafe "<![endif]-->" }}      

模闆變量

. 字元

模闆變量可以是boolean, string, character, integer, floating-point, imaginary 或者 complex constant。傳給模闆這樣的資料就可以通過點号​

​.​

​來通路:

{ { . }}      

如果資料是複雜類型的資料,可以通過​

​{ { .FieldName }}​

​來通路它的字段。

如果字段還是複雜類型,可以鍊式通路 ​

​{ { .Struct.StructTwo.Field }}​

​。

模闆中的變量

傳給模闆的資料可以存在模闆中的變量中,在整個模闆中都能通路。 比如 ​

​{ {$number := .}}​

​​, 我們使用​

​$number​

​​作為變量,儲存傳入的資料,可以使用​

​{ {$number}}​

​來通路變量。

{ {$number := .}}
<h1> It is day number { {$number}} of the month </h1>
var tpl *template.Template

tpl = template.Must(template.ParseFiles("templateName"))

err := tpl.ExecuteTemplate(os.Stdout, "templateName", 23)      

上面的例子我們把23傳給模闆,模闆的變量​

​$number​

​的值是23,可以在模闆中使用。

模闆動作

if/else 語句

像其它語言,模闆支援​

​if/else​

​語句。我們可以使用if檢查資料,如果不滿足可以執行else。空值是是false, 0、nil、空字元串或者長度為0的字元串都是false。

<h1>Hello, { {if .Name}} { {.Name}} { {else}} Anonymous { {end}}!</h1>      

如果​

​.Name​

​​存在,會輸出​

​Hello, Name​

​​,否則輸出​

​Hello, Anonymous​

​。

模闆也提供了​

​{ {else if .Name2 }}​

​處理多個分支。

移除空格

往模闆中增加不同的值的時候可能會增加一定數量的空格。我們既可以改變我們的模闆以便更好的處理它,忽略/最小化這種效果,或者我們還可以使用減号​

​-​

​:

<h1>Hello, { {if .Name}} { {.Name}} { {- else}} Anonymous { {- end}}!</h1>      

上面的例子告訴模闆移除 ​

​.Name​

​變量之間的空格。我們在end關鍵字中也加入減号。這樣做的好處是在模闆中我們通過空格更友善程式設計調試,但是生産環境中我們不需要空格。

Range

模闆提供​

​range​

​關鍵字來周遊資料。假如我們又下面的資料結構:

type Item struct {
  Name  string
  Price int
}

type ViewData struct {
  Name  string
  Items []Item
}      

​ViewData​

​對象傳給模闆,模闆如下:

{ {range .Items}}
  <div class="item">
    <h3 class="name">{ {.Name}}</h3>
    <span class="price">${ {.Price}}</span>
  </div>
{ {end}}      

對于Items中的每個Item, 我們輸出它的名稱和價格。在range中目前的項目變成了​

​{ {.}}​

​​,它的屬性是​

​{ {.Name}}​

​​和​

​{ {.Price}}​

​。

模闆函數

模闆包提供了一組預定義的函數,下面介紹一些常用的函數。

擷取索引值

如果傳給模闆的資料是map、slice、數組,那麼我們就可以使用它的索引值。我們使用​

​{ {index x number}}​

​​來通路​

​x​

​​的第​

​number​

​​個元素, ​

​index​

​​是關鍵字。比如​

​{ {index names 2}}​

​​等價于​

​names[2]​

​​。​

​{ {index names 2 3 4}}​

​​ 等價于 ​

​names[2][3][4]​

​。

<body>
    <h1> { {index .FavNums 2 }}</h1>
</body>
type person struct {
    Name    string
    FavNums []int
}

func main() {

    tpl := template.Must(template.ParseGlob("*.gohtml"))
    tpl.Execute(os.Stdout, &person{"Curtis", []int{7, 11, 94}})
}      

上面的例子傳入一個person的資料結構,得到它的FavNums字段中的第三個值。

and 函數

and函數傳回bool值,通過傳回第一個空值或者最後一個值。​

​and x y​

​​邏輯上相當于​

​if x then y else x​

​。考慮下面的代碼:

type User struct {  
  Admin bool
}

type ViewData struct {  
  *User
}      

傳入一個Admin為true的ViewData對象給模闆:

{ {if and .User .User.Admin}}
  You are an admin user!
{ {else}}
  Access denied!
{ {end}}      

結果會顯示​

​You are an admin user!​

​​, 如果ViewData不包含一個User值,或者Admin為false,顯示結果則會是​

​Access denied!​

​。

or 函數

類似 and 函數,但是隻要遇到 true就傳回。​

​or x y​

​​ 等價于 ​

​if x then x else y​

​。 x 非空的情況下y不會被評估。

not 函數

not函數傳回參數的相反值:

{ { if not .Authenticated}}
  Access Denied!
{ { end }}      

管道

函數調用可以鍊式調用,前一個函數的輸出結果作為下一個函數調用的參數。​

​html/template​

​​稱之為管道,類似于linux shell指令中的管道一樣,它采用​

​|​

​分隔。

注意前一個指令的輸出結果是作為下一個指令的最後一個參數,最終指令的輸出結果就是這個管道的結果。

模闆比較函數

比較

​html/template​

​​提供了一系列的函數用做資料的比較。資料的類型隻能是基本類型和命名的基本類型,比如​

​type Temp float3​

​​,格式是​

​{ { function arg1 arg2 }}​

​。

  • ​eq​

    ​: arg1 == arg2
  • ​ne​

    ​: arg1 != arg2
  • ​lt​

    ​: arg1 < arg2
  • ​le​

    ​: arg1 <= arg2
  • ​gt​

    ​: arg1 > arg2
  • ​ge​

    ​: arg1 >= arg2

​eq​

​​函數比較特殊,可以拿多個參數和第一個參數進行比較。​

​{ { eq arg1 arg2 arg3 arg4}}​

​​邏輯是​

​arg1==arg2 || arg1==arg3 || arg1==arg4​

​。

嵌套模闆和布局

嵌套模闆

嵌套模闆可以用做跨模闆的公共部分代碼,比如 header或者 footer。使用嵌套模闆我們就可以避免一點小小的改動就需要修改每個模闆。嵌套模闆定義如下:

{ {define "footer"}}
<footer> 
  <p>Here is the footer</p>
</footer>
{ {end}}      

這裡定義了一個名為​

​footer​

​的模闆,可以在其他模闆中使用:

{ {template "footer"}}      

模闆之間傳遞變量

模闆action可以使用第二個參數傳遞資料給嵌套的模闆:

// Define a nested template called header
{ {define "header"}}
  <h1>{ {.}}</h1>
{ {end}}

// Call template and pass a name parameter
{ {range .Items}}
  <div class="item">
    { {template "header" .Name}}
    <span class="price">${ {.Price}}</span>
  </div>
{ {end}}      

這裡我們使用和上面一樣的range周遊items,但是我們會把每個name傳給header模闆。

建立布局

Glob模式通過通配符比對一組檔案名。​

​template.ParseGlob(pattern string)​

​​會比對所有符合模式的模闆。​

​template.ParseFiles(files...)​

​​也可以用來解析一組檔案。

模闆預設情況下會使用配置的參數檔案名的base name作為模闆名。這意味着​

​views/layouts/hello.gohtml​

​​的檔案名是​

​hello.gohtml​

​​,如果模闆中有​

​{ {define “templateName”}}​

​​的話,那麼​

​templateName​

​會用作這個模闆的名字。

模闆可以通過​

​t.ExecuteTemplate(w, "templateName", nil)​

​​來執行, t是一個類型為​

​Template​

​​的對象,​

​w​

​​的類型是​

​io.Writer​

​​,比如​

​http.ResponseWriter​

​,然後是要執行的模闆的名稱,以及要傳入的資料:

main.go
// Omitted imports & package

var LayoutDir string = "views/layouts"  
var bootstrap *template.Template

func main() {
  var err error
  bootstrap, err = template.ParseGlob(LayoutDir + "/*.gohtml")
  if err != nil {
    panic(err)
  }

  http.HandleFunc("/", handler)
  http.ListenAndServe(":8080", nil)
}

func handler(w http.ResponseWriter, r *http.Request) {
  bootstrap.ExecuteTemplate(w, "bootstrap", nil)
}      

所有的​

​.gohtml​

​​檔案都被解析,然後當通路​

​/​

​​的時候,​

​bootstrap​

​會被執行。

​views/layouts/bootstrap.gohtml​

​定義如下:

views/layouts/bootstrap.gohtml
{ {define "bootstrap"}}
<!DOCTYPE html>  
<html lang="en">  
  <head>
    <title>Go Templates</title>
    <link href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" 
  rel="stylesheet">
  </head>
  <body>
    <div class="container-fluid">
      <h1>Filler header</h1>
    <p>Filler paragraph</p>
    </div>
    <!-- jquery & Bootstrap JS -->
    <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"  
    </script>
    <script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js">
    </script>
  </body>
</html>  
{ {end}}      

模闆調用函數

函數變量 (調用結構體的方法)

我們可以調用模闆中對象的方法傳回資料,下面定義了User類型,以及一個方法:

type User struct {  
  ID    int
  Email string
}

func (u User) HasPermission(feature string) bool {  
  if feature == "feature-a" {
    return true
  } else {
    return false
  }
}      

當User類型傳給模闆後,我們可以在模闆中調用它的方法:

{ {if .User.HasPermission "feature-a"}}
  <div class="feature">
    <h3>Feature A</h3>
    <p>Some other stuff here...</p>
  </div>
{ {else}}
  <div class="feature disabled">
    <h3>Feature A</h3>
    <p>To enable Feature A please upgrade your plan</p>
  </div>
{ {end}}      

模闆會調用User的HasPermission方法做檢查,并且根據這個傳回結果渲染資料。

函數變量 (調用)

如果有時HasPermission方法的設計不得不需要更改,但是目前的函數方法有不滿足要求,我們可以使用函數(​

​func(string) bool​

​)作為User類型的字段,這樣在建立User的時候可以指派不同的函數實作:

// Structs
type ViewData struct {  
  User User
}

type User struct {  
  ID            int
  Email         string
  HasPermission func(string) bool
}

// Example of creating a ViewData
vd := ViewData{
    User: User{
      ID:    1,
      Email: "[email protected]",
      // Create the HasPermission function
      HasPermission: func(feature string) bool {
        if feature == "feature-b" {
          return true
        }
        return false
      },
    },
  }

// Executing the ViewData with the template
err := testTemplate.Execute(w, vd)      

我們需要告訴Go模闆我們想調用這個函數,這裡使用​

​call​

​關鍵字。把上面的例子修改如下:

{ {if (call .User.HasPermission "feature-b")}}
  <div class="feature">
    <h3>Feature B</h3>
    <p>Some other stuff here...</p>
  </div>
{ {else}}
  <div class="feature disabled">
    <h3>Feature B</h3>
    <p>To enable Feature B please upgrade your plan</p>
  </div>
{ {end}}      

自定義函數

另外一種方式是使用​

​template.FuncMap​

​​建立自定義的函數,它建立一個全局的函數,可以在整個應用中使用。​

​FuncMap​

​​通過​

​map[string]interface{}​

​​将函數名映射到函數上。注意映射的函數必須隻有一個傳回值,或者有兩個傳回值但是第二個是​

​error​

​類型。

// Creating a template with function hasPermission
testTemplate, err = template.New("hello.gohtml").Funcs(template.FuncMap{
    "hasPermission": func(user User, feature string) bool {
      if user.ID == 1 && feature == "feature-a" {
        return true
      }
      return false
    },
  }).ParseFiles("hello.gohtml")      

這個函數​

​hasPermission​

​​檢查使用者是否有某個權限,它會被儲存在​

​FuncMap​

​​中。注意自定義的函數必須在調用​

​ParseFiles()​

​之前建立。

這個函數在模闆中的使用如下:

{ { if hasPermission .User "feature-a" }}      

需要傳入​

​.User​

​​和​

​feature-a​

​參數。

自定義函數 (全局)

我們前面實作的自定義方法需要依賴​

​.User​

​類型,很多情況下這種方式工作的很好,但是在一個大型的應用中傳給模闆太多的對象維護起來很困難。我們需要改變自定義的函數,讓它無需依賴User對象。

和上面的實作類似,我們建立一個預設的​

​hasPermission​

​函數,這樣可以正常解析模闆。

testTemplate, err = template.New("hello.gohtml").Funcs(template.FuncMap{
  "hasPermission": func(feature string) bool {
    return false
  },
}).ParseFiles("hello.gohtml")      

這個函數在​

​main()​

​中或者某處建立,并且保證在解析檔案之前放入到 hello.gohtml 的function map中。這個預設的函數總是傳回false,但是不管怎樣,函數是已定義的,而且不需要User,模闆也可以正常解析。

下一個技巧就是重新定義​

​hasPermission​

​函數。這個函數可以使用User對象的資料,但是它是在Handler進行中使用的,而不是傳給模闆,這裡采用的是閉包的方式。是以在模闆執行之前你死有機會重新定義函數的。

func handler(w http.ResponseWriter, r *http.Request) {
  w.Header().Set("Content-Type", "text/html")

  user := User{
    ID:    1,
    Email: "[email protected]",
  }
  vd := ViewData{}
  err := testTemplate.Funcs(template.FuncMap{
    "hasPermission": func(feature string) bool {
      if user.ID == 1 && feature == "feature-a" {
        return true
      }
      return false
    },
  }).Execute(w, vd)
  if err != nil {
    http.Error(w, err.Error(), http.StatusInternalServerError)
  }
}      

在這個Handler中User被建立,ViewData使用這個User對象。​

​hasPermission​

​​采用閉包的方式重新定義了函數。​

​{ {if hasPermission "feature-a"}}​

​的的确确沒有傳入User參數。

第三方自定義函數

除了官方的預定義的函數外,一些第三方也定義了一些函數,你可以使用這些庫,避免重複造輪子。

比如​​sprig​​庫,定義了很多的函數:

  • String Functions

    :

trim      

,

wrap      

,

randAlpha      

,

plural      

, etc.

  • ​​String List Functions​​​:​

    ​splitList​

    ​​,​

    ​sortAlpha​

    ​, etc.
  • Math Functions

    :

add      

,

max      

,

mul      
  • ​​Integer Slice Functions​​​:​

    ​until​

    ​​,​

    ​untilStep​

  • ​​Date Functions​​​:​

    ​now​

    ​​,​

    ​date​

    ​, etc.
  • ​​Defaults Functions​​​:​

    ​default​

    ​​,​

    ​empty​

    ​​,​

    ​coalesce​

    ​​,​

    ​toJson​

    ​​,​

    ​toPrettyJson​

    ​​,​

    ​toRawJson​

    ​​,​

    ​ternary​

  • ​​Encoding Functions​​​:​

    ​b64enc​

    ​​,​

    ​b64dec​

    ​, etc.
  • ​​Lists and List Functions​​​:​

    ​list​

    ​​,​

    ​first​

    ​​,​

    ​uniq​

    ​, etc.
  • ​​Dictionaries and Dict Functions​​​:​

    ​get​

    ​​,​

    ​set​

    ​​,​

    ​dict​

    ​​,​

    ​hasKey​

    ​​,​

    ​pluck​

    ​​,​

    ​deepCopy​

    ​, etc.
  • ​​Type Conversion Functions​​​:​

    ​atoi​

    ​​,​

    ​int64​

    ​​,​

    ​toString​

    ​, etc.
  • ​​File Path Functions​​​:​

    ​base​

    ​​,​

    ​dir​

    ​​,​

    ​ext​

    ​​,​

    ​clean​

    ​​,​

    ​isAbs​

  • ​​Flow Control Functions​​​:​

    ​fail​

  • Advanced Functions
  • ​​UUID Functions​​​:​

    ​uuidv4​

  • ​​OS Functions​​​:​

    ​env​

    ​​,​

    ​expandenv​

  • ​​Version Comparison Functions​​​:​

    ​semver​

    ​​,​

    ​semverCompare​

  • ​​Reflection​​​:​

    ​typeOf​

    ​​,​

    ​kindIs​

    ​​,​

    ​typeIsLike​

    ​, etc.
  • ​​Cryptographic and Security Functions​​​:​

    ​derivePassword​

    ​​,​

    ​sha256sum​

    ​​,​

    ​genPrivateKey​

    ​, etc.