39.Go Text和HTML模板

假设您正在使用Web应用程序,并且需要返回列出最新推文的HTML。 您需要从数据库加载推文列表,并根据该信息创建HTML。

Go标准库中的text/template和html/template包可以实现数据驱动模板以生成文本输出:

var tmplStr = `User {{.User}} has {{.TotalTweets}} tweets.
{{- $tweetCount := len .RecentTweets }}
Recent tweets:
{{range $idx, $tweet := .RecentTweets}}Tweet {{$idx}} of {{$tweetCount}}: '{{.}}'
{{end -}}
Most recent tweet: '{{index .RecentTweets 0}}'

t := template.New("tweets")
t, err := t.Parse(tmplStr)
if err != nil {
    log.Fatalf("template.Parse() failed with '%s'\n", err)
}
data := struct {
    User         string
    TotalTweets  int
    RecentTweets []string
}{
    User:         "kjk",
    TotalTweets:  124,
    RecentTweets: []string{"hello", "there"},
}
err = t.Execute(os.Stdout, data)
if err != nil {
    log.Fatalf("t.Execute() failed with '%s'\n", err)
}

User kjk has 124 tweets.
Recent tweets:
Tweet 0 of 2: 'hello'
Tweet 1 of 2: 'there'
Most recent tweet: 'hello'

每个模板都有名字. template.New("tweets") 创建了空模板tweets.

t.Parse(s string) 用来解析模板.

t.Execute(w io.Writer, v interface{}) 使用数据填充模板并将结果写入io.Writer.

{{ ... }} 包裹的内容将被执行替换, 如{{.TweetCount}}

Data passed to a template can be hierarchical (i.e. a struct withing a struct within a struct…).

Current context . refers to current scope within the data.

Initial . refers to top-level scope:

tmplStr := "Data: {{.}}\n"
t := template.Must(template.New("simple").Parse(tmplStr))
execWithData := func(data interface{}) {
    err := t.Execute(os.Stdout, data)
    if err != nil {
        log.Fatalf("t.Execute() failed with '%s'\n", err)
    }
}

execWithData(5)
execWithData("foo")
st := struct {
    Number int
    Str    string
}{
    Number: 3,
    Str:    "hello",
}
execWithData(st)

Data: 5
Data: foo
Data: {3 hello}

Values that don’t have pre-defined formatting are printed using Stringer interface. For custom formatting of your type in a template implement String() string method.

{{range .Tweets}}{{end}} evaluates inner part for every element of []string slice Tweets and sets current context . within the inner part to elements of Tweets slice.

{{index .RecentTweets 0}} is equivalent to RecentTweets[0] in Go code.

Text in a template is copied verbatim. Having to preserve whitespace can lead to ugly templates.

To help write more readable templates We can add - at the beginning or end of action as seen in {{end -}}.

This remove whitespace before or after the action.

{{range .RecentTweets}} changes variable scope and we don’t have access to data outside. If we need to access data from upper scope, we can define variables like {{ $tweetCount := len .RecentTweets }}.

Methods as data

In a template {{ .Foo }} will either access struct field Foo or call a function Foo():

var tmplStr = Data from a field: '{{ .Field }}' Data from a method: '{{ .Method }}'

t := template.New("method")
t, err := t.Parse(tmplStr)
if err != nil {
log.Fatalf("template.Parse() failed with '%s'\n", err)
}

data := Data{
Field: 5,
}

err = t.Execute(os.Stdout, data)
if err != nil {
log.Fatalf("t.Execute() failed with '%s'\n", err)
}

Data from a field: '5'
Data from a method: 'data from a method'

## if action
To conditionally render parts of the template, use if action:

const tmplStr = `{{range . -}}
{{if .IsNew}}'{{.Name}}' is new{{else}}'{{.Name}}' is not new{{end}}
{{end}}`

t := template.Must(template.New("if").Parse(tmplStr))

data := []struct {
    Name  string
    IsNew bool
}{
    {"Bridge", false},
    {"Electric battery", true},
}

err := t.Execute(os.Stdout, data)
if err != nil {
    log.Fatalf("t.Execute() failed with '%s'\n", err)
}

'Bridge' is not new
'Electric battery' is new
false values
Templating uses “truthy” logic for deciding what values are true or false in the context of if action:

const tmplStr = `{{range . -}}
{{printf "%- 16s" .Name}} is: {{if .Value}}true{{else}}false{{end}}
{{end}}`

t := template.Must(template.New("if").Parse(tmplStr))

var nilPtr *string = nil
var nilSlice []float32
emptySlice := []int{}

data := []struct {
    Name  string
    Value interface{}
}{
    {"bool false", false},
    {"bool true", true},
    {"integer 0", 0},
    {"integer 1", 1},
    {"float32 0", float32(0)},
    {"float64 NaN", math.NaN},
    {"empty string", ""},
    {"non-empty string", "haha"},
    {"nil slice", nilSlice},
    {"empty slice", emptySlice},
    {"non-empty slice", []int{3}},
    {"nil pointer", nilPtr},
}

err := t.Execute(os.Stdout, data)
if err != nil {
    log.Fatalf("t.Execute() failed with '%s'\n", err)
}
bool false       is: false
bool true        is: true
integer 0        is: false
integer 1        is: true
float32 0        is: false
float64 NaN      is: true
empty string     is: false
non-empty string is: true
nil slice        is: false
empty slice      is: false
non-empty slice  is: true
nil pointer      is: false

Avoid printing empty slices
Truthy logic is useful when we want to show different text if a list of items is empty:

type UserTweets struct {
    User   string
    Tweets []string
}

const tmplStr = `
{{- if not .Tweets -}}
User '{{.User}}' has no tweets.
{{ else -}}
User '{{.User}}' has {{ len .Tweets }} tweets:
{{ range .Tweets -}}
  '{{ . }}'
{{ end }}
{{- end}}`

t := template.Must(template.New("if").Parse(tmplStr))

data := UserTweets{
    User: "kjk",
}
err := t.Execute(os.Stdout, data)
if err != nil {
    log.Fatalf("t.Execute() failed with '%s'\n", err)
}

data = UserTweets{
    User:   "masa",
    Tweets: []string{"tweet one", "tweet two"},
}
err = t.Execute(os.Stdout, data)
if err != nil {
    log.Fatalf("t.Execute() failed with '%s'\n", err)
}

User 'kjk' has no tweets.
User 'masa' has 2 tweets:
'tweet one'
'tweet two'

range action

Just as in Go, range can iterate over arrays, slices, maps and channels.

对数组的迭代

const tmplStr = `Elements of arrays or slice: {{ range . }}{{ . }} {{end}}
t := template.Must(template.New("range").Parse(tmplStr))

array := [...]int{3, 8}
err := t.Execute(os.Stdout, array)
if err != nil {
    log.Fatalf("t.Execute() failed with '%s'\n", err)
}

slice := []int{12, 5}
err = t.Execute(os.Stdout, slice)
if err != nil {
    log.Fatalf("t.Execute() failed with '%s'\n", err)
}

Elements of arrays or slice: 3 8
Elements of arrays or slice: 12 5

对map的迭代

const tmplStr = `Elements of map:
{{ range $k, $v := . }}{{ $k }}: {{ $v }}
{{end}}`

t := template.Must(template.New("range").Parse(tmplStr))

data := map[string]int{
    "one":  1,
    "five": 5,
}
err := t.Execute(os.Stdout, data)
if err != nil {
    log.Fatalf("t.Execute() failed with '%s'\n", err)
}

Elements of map:
five: 5
one: 1

对channel的迭代

const tmplStr = `Elements of a channel: {{ range . }}{{ . }} {{end}}
t := template.Must(template.New("range").Parse(tmplStr))

ch := make(chan int)
go func() {
    for i := 0; i < 3; i++ {
        ch <- i
    }
    close(ch)
}()
err := t.Execute(os.Stdout, ch)
if err != nil {
    log.Fatalf("t.Execute() failed with '%s'\n", err)
}

Elements of a channel: 0 1 2

内建函数

Templating engine supports calling functions like {{ len .Tweet }} where len is a function that returns length of an array or slice.

and, or, not
and, or, not are for logical operations:

const tmplStr = `Or:  {{ if or .True .False }}true{{ else }}false{{ end }}
And: {{ if and .True .False }}true{{ else }}false{{ end }}
Not: {{ if not .False }}true{{ else }}false{{ end }}
t := template.Must(template.New("and_or_not").Parse(tmplStr))

data := Data{True: true, False: false}

err := t.Execute(os.Stdout, data)
if err != nil {
    log.Fatalf("t.Execute() failed with '%s'\n", err)
}

Or: true
And: false
Not: true

index

index is for accessing elements of a slice by index or values in a map by key.

const tmplStr = `Slice[0]: {{ index .Slice 0 }}
SliceNested[1][0]: {{ index .SliceNested 1 0 }}
Map["key"]: {{ index .Map "key" }}
t := template.Must(template.New("index").Parse(tmplStr))

data := struct {
    Slice       []string
    SliceNested [][]int
    Map         map[string]int
}{
    Slice: []string{"first", "second"},
    SliceNested: [][]int{
        {3, 1},
        {2, 3},
    },
    Map: map[string]int{
        "key": 5,
    },
}
err := t.Execute(os.Stdout, data)
if err != nil {
    log.Fatalf("t.Execute() failed with '%s'\n", err)
}

Slice[0]: first
SliceNested[1][0]: 2
Map["key"]: 5

len

返回数组或map的长度

const tmplStr = `len(nil)       : {{ len .SliceNil }}
len(emptySlice): {{ len .SliceEmpty }}
len(slice)     : {{ len .Slice }}
len(map)       : {{ len .Map }}
`

t := template.Must(template.New("len").Parse(tmplStr))

data := struct {
    SliceNil   []int
    SliceEmpty []string
    Slice      []bool
    Map        map[int]bool
}{
    SliceNil:   nil,
    SliceEmpty: []string{},
    Slice:      []bool{true, true, false},
    Map:        map[int]bool{5: true, 3: false},
}
err := t.Execute(os.Stdout, data)
if err != nil {
    log.Fatalf("t.Execute() failed with '%s'\n", err)
}

len(nil) : 0
len(emptySlice): 0
len(slice) : 3
len(map) : 2

print, printf, println

print 作用同 fmt.Sprint.
printf 作用同 fmt.Sprintf.
println 作用同 fmt.Sprintln.

const tmplStr = `print:   {{ print .Str .Num }}
println: {{ println .Str .Num }}
printf:  {{ printf "%s %#v %d" .Str .Str .Num }}
`
t := template.Must(template.New("print").Parse(tmplStr))

data := struct {
    Str string
    Num int
}{
    Str: "str",
    Num: 8,
}
err := t.Execute(os.Stdout, data)
if err != nil {
    log.Fatalf("t.Execute() failed with '%s'\n", err)
}

print: str8
println: str 8
printf: str "str" 8

js, html, urlquery

js, html and urlquery is for escaping text so that it can be safely inserted in a JavaScript, HTML and URL context:

const tmplStr = `js escape  : {{ js .JS }}
html escape: {{ html .HTML }}
url escape : {{ urlquery .URL }}
`
t := template.Must(template.New("print").Parse(tmplStr))

data := struct {
    JS   string
    HTML string
    URL  string
}{
    JS:   `function me(s) { return "foo"; }`,
    HTML: `<div>text</div>`,
    URL:  `http://www.programming-books.io`,
}
err := t.Execute(os.Stdout, data)
if err != nil {
    log.Fatalf("t.Execute() failed with '%s'\n", err)
}

js escape : function me(s) { return "foo"; }
html escape: <div>text</div>
url escape : http%3A%2F%2Fwww.programming-books.io

自定义函数

你可以自定义函数, 然后在模板中使用

const tmplStr = `5 + 5 = {{ sum 5 .Arg }}`

customFunctions := template.FuncMap{
    "sum": sum,
}

t := template.Must(template.New("func").Funcs(customFunctions).Parse(tmplStr))

data := struct {
    Arg int
}{
    Arg: 5,
}
err := t.Execute(os.Stdout, data)
if err != nil {
    log.Fatalf("t.Execute() failed with '%s'\n", err)
}

5 + 5 = 10

HTML 模板

html/template包和text/template包有相同的功能.

The difference is that html/template understands structure of HTML and JavaScript code inside HTML.

Inserted text is escaped based on its surrounding context which eliminates cross-site scripting bugs.

const tmplStr = `<div onlick="{{ .JS }}">{{ .HTML }}</div>
`
txt := text_template.Must(text_template.New("text").Parse(tmplStr))
html := html_template.Must(html_template.New("html").Parse(tmplStr))
data := struct {
    JS   string
    HTML string
    URL  string
}{
    JS:   `foo`,
    HTML: `<span>text</span>`,
    URL:  `http://www.programming-books.io`,
}

err := txt.Execute(os.Stdout, data)
if err != nil {
    log.Fatalf("t.Execute() failed with '%s'\n", err)
}

fmt.Println()

err = html.Execute(os.Stdout, data)
if err != nil {
    log.Fatalf("t.Execute() failed with '%s'\n", err)
}

<div onlick="foo"><span>text</span></div>

<div onlick=""foo""><span>text</span></div>

Inserting unescaped HTML

Sometimes you need to subvert escaping of text:

const tmplStr = `<div onlick="{{ .JS }}">{{ .HTML }}</div>
`

html := template.Must(template.New("html").Parse(tmplStr))

data := struct {
    JS   string
    HTML string
}{
    JS:   `foo`,
    HTML: `<span>text</span>`,
}

fmt.Printf("Escaped:\n")
err := html.Execute(os.Stdout, data)
if err != nil {
    log.Fatalf("t.Execute() failed with '%s'\n", err)
}

fmt.Printf("\nUnescaped:\n")
data2 := struct {
    JS   template.JS
    HTML template.HTML
}{
    JS:   `foo`,
    HTML: `<span>text</span>`,
}
err = html.Execute(os.Stdout, data2)
if err != nil {
    log.Fatalf("t.Execute() failed with '%s'\n", err)
}

Escaped:
<div onlick=""foo""><span>text</span></div>

Unescaped:
<div onlick="foo"><span>text</span></div>

template.HTML and template.JS are type alises for string so you can assign string values to them.

Templating engine recognizes those types and disables escaping for them.

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343

推荐阅读更多精彩内容