第 03 课:进阶模板用法

Unknwon · 2019-11-10 08:55:32 · 3338 次点击 · 预计阅读时间 12 分钟 · 大约8小时之前 开始浏览    
这是一个创建于 2019-11-10 08:55:32 的文章,其中的信息可能已经有所发展或是发生改变。

在上一节课中,我们学习了标准库中 text/template 包提供的文本模板引擎的基础用法,了解了模板渲染和根对象的概念。这节课,我们将基于上节课的知识,进一步学习如何在 Go 语言提供的模板引擎中进行条件判断和更加复杂的逻辑操作。

在模板中定义变量

变量不仅是 Go 语言中程序代码的重要组成部分,同样也是模板引擎中的主要元素。因为只有通过定义和操作变量,才能使得模板引擎在逻辑和用法上更加灵活和便利。

text/template 包提供的文本模板引擎支持使用字母数字(Alphanumeric)作为变量的名称,并使用一个美元符号($)作为前缀,例如:$name$age$round2。在模板中的定义语法和程序代码中类似,即使用 := 连接变量名和赋值语句。

package main

import (
    "fmt"
    "log"
    "net/http"
    "text/template"
)

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        // 创建模板对象并解析模板内容
        tmpl, err := template.New("test").Parse(`
{{$name := "Alice"}}
{{$age := 18}}
{{$round2 := true}}
Name: {{$name}}
Age: {{$age}}
Round2: {{$round2}}
`)
        if err != nil {
            fmt.Fprintf(w, "Parse: %v", err)
            return
        }

        // 调用模板对象的渲染方法
        err = tmpl.Execute(w, nil)
        if err != nil {
            fmt.Fprintf(w, "Execute: %v", err)
            return
        }
    })

    log.Println("Starting HTTP server...")
    log.Fatal(http.ListenAndServe("localhost:4000", nil))
}

尝试运行以上代码可以在终端获得以下结果:

➜ curl http://localhost:4000
Name: Alice
Age: 18
Round2: true

不难发现,这个示例的核心就是包含变量使用的模板内容:

{{$name := "Alice"}}
{{$age := 18}}
{{$round2 := true}}
Name: {{$name}}
Age: {{$age}}
Round2: {{$round2}}

在这里,我们需要注意的是以下三点:

  1. 变量的定义(或首次获得赋值)必须使用 := 的语法。
  2. 获取变量值时,直接在相应位置使用美元符号加上变量名称即可。
  3. 所有有关变量的操作都属于模板语法的一部分,因此需要使用双层大括号将其包裹起来。

那么,在变量被定义之后,如何修改变量的值呢?很简单,只需要和程序代码中那样,直接使用等号(=)即可。

...
        // 创建模板对象并解析模板内容
        tmpl, err := template.New("test").Parse(`
{{$name := "Alice"}}
{{$age := 18}}
{{$round2 := true}}
Name: {{$name}}
Age: {{$age}}
Round2: {{$round2}}

{{$name = "Bob"}}
Name: {{$name}}
`)
...

(为了缩减篇幅并更好地专注于有变动的部分,部分未改动的代码块使用了 “…” 进行替代。)

尝试运行变动后的代码可以在终端获得以下结果:

➜ curl http://localhost:4000
Name: Alice
Age: 18
Round2: true
Name: Bob

感兴趣的同学可以尝试一下,如果重复使用 := 的语法给相同名称的变量多次赋值会发生什么呢?

在模板中使用条件判断(if 语句)

标准库 text/template 包供的文本模板引擎除了可以进行单纯的数据展示外,还能够像程序代码那样进行基本的逻辑控制,而逻辑控制语句中最常见的便是 if 语句了。

接下来,我们需要编写一个能够进行除法运算的 Web 服务,即通过 URL 查询参数接收两个值,分别为 x 和 y(被除数与除数),然后进行 x/y 的运算,再将运算结果返回给客户端。由于除法的特殊性,当 y 为 0 的时候是无法进行运算的。因此,我们需要在 y 等于 0 的时候提示客户端参数错误(利用模板的 if 语句)。

package main

import (
    ...
    "strconv"
)

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        // 创建模板对象并解析模板内容
        tmpl, err := template.New("test").Parse(`
{{if .yIsZero}}
    除数不能为 0
{{else}}
    {{.result}}
{{end}}
`)
        if err != nil {
            fmt.Fprintf(w, "Parse: %v", err)
            return
        }

        // 获取 URL 查询参数的值
        // 注意:为了简化代码逻辑,这里并没有进行错误处理
        x, _ := strconv.ParseInt(r.URL.Query().Get("x"), 10, 64)
        y, _ := strconv.ParseInt(r.URL.Query().Get("y"), 10, 64)

        // 当 y 不为 0 时进行除法运算
        yIsZero := y == 0
        result := 0.0
        if !yIsZero {
            result = float64(x) / float64(y)
        }

        // 调用模板对象的渲染方法
        err = tmpl.Execute(w, map[string]interface{}{
            "yIsZero": yIsZero,
            "result":  result,
        })
        if err != nil {
            fmt.Fprintf(w, "Execute: %v", err)
            return
        }
    })

    ...
}

(为了缩减篇幅并更好地专注于有变动的部分,部分未改动的代码块使用了 “…” 进行替代。)

以上代码的逻辑非常简单,即首先通过程序判断除数 y 是否为 0,然后将判断结果和可能的除法运算结果都赋值到 map 类型的根对象中。

在模板中,我们需要将条件语句放置在 if 关键字之后,使用空格将它们分隔,并将整个语句使用分隔符 {{}} 进行包裹。需要注意的是,条件语句必须要返回一个布尔类型(bool)的值,本例中 yIsZero 变量自身即是 bool 类型的值,因此不需要再做额外的类型转换。

尝试运行以上代码可以在终端获得以下结果:

➜ curl http://localhost:4000?x=1&y=2
0.5
➜ curl http://localhost:4000?x=1&y=0
除数不能为 0

本例中展示的条件语句十分简单,但在实际开发过程中,if 语句通常会被大量使用,然后根据给定的条件判断渲染出不同的内容。

模板中的等式与不等式

如果所有的条件判断都只能在程序代码中完成,然后直接输出给模板计算好的条件,未免有点太不方便了。因此,Go 语言的文本模板引擎同样可以在模板中完成等式与不等式的判断,为更加复杂的条件判断提供了必要的支持。

用于等式与不等式判断的函数主要有以下六种(均接受两个,分别名为 arg1arg2 的参数):

  • eq:当等式 arg1 == arg2 成立时,返回 true,否则返回 false
  • ne:当不等式 arg1 != arg2 成立时,返回 true,否则返回 false
  • lt:当不等式 arg1 < arg2 成立时,返回 true,否则返回 false
  • le:当不等式 arg1 <= arg2 成立时,返回 true,否则返回 false
  • gt:当不等式 arg1 > arg2 成立时,返回 true,否则返回 false
  • ge:当不等式 arg1 >= arg2 成立时,返回 true,否则返回 false

如果你对这些函数的名字感到奇怪,其实不难发现这些名字本质上就是相关英文的缩写。如 “eq” 是 “equal” 的缩写,”ne” 表示 “not equal”,”lt” 表示 “less than“,”le” 表示 “less than or equal” 等等。

接下来,我们就结合目前所学的知识,将更多的判断逻辑放置到模板当中完成。

package main

import (
    "fmt"
    "log"
    "net/http"
    "text/template"
)

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        // 创建模板对象并解析模板内容
        tmpl, err := template.New("test").Parse(`
{{$name1 := "alice"}}
{{$name2 := "bob"}}
{{$age1 := 18}}
{{$age2 := 23}}

{{if eq $age1 $age2}}
    年龄相同
{{else}}
    年龄不相同
{{end}}

{{if ne $name1 $name2}}
    名字不相同
{{end}}

{{if gt $age1 $age2}}
    alice 年龄比较大
{{else}}
    bob 年龄比较大
{{end}}
`)
        if err != nil {
            fmt.Fprintf(w, "Parse: %v", err)
            return
        }

        // 调用模板对象的渲染方法
        err = tmpl.Execute(w, nil)
        if err != nil {
            fmt.Fprintf(w, "Execute: %v", err)
            return
        }
    })

    log.Println("Starting HTTP server...")
    log.Fatal(http.ListenAndServe("localhost:4000", nil))
}

在这个例子中,我们使用到了 eqnegt 三个函数。尝试运行以上代码可以在终端获得以下结果:

➜ curl http://localhost:4000
年龄不相同
名字不相同
bob 年龄比较大

你可能会对这个例子中 if 条件语句的用法感到怪异,这是因为 eqnegt 等本质上属于函数,而函数的调用都是以 函数名称(参数 1,参数 2,...) 的形式,只是在大部分情况下,Go 语言标准库提供的这套模板引擎可以在语法上省略括号的使用。

在模板中使用迭代操作(range 语句)

除了可以在模板中进行条件判断以外,Go 语言标准库提供的模板引擎还支持通过 range 语句进行迭代操作,以方便直接在模板中对集合类型的数据进行处理和渲染。

Go 语言中一般来说有三种类型可以进行迭代操作,数组(Array)、切片(Slice)和 map 类型。

package main

import (
    "fmt"
    "log"
    "net/http"
    "text/template"
)

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        // 创建模板对象并解析模板内容
        tmpl, err := template.New("test").Parse(`
{{range $name := .Names}}
    {{$name}}
{{end}}
`)
        if err != nil {
            fmt.Fprintf(w, "Parse: %v", err)
            return
        }

        // 调用模板对象的渲染方法
        err = tmpl.Execute(w, map[string]interface{}{
            "Names": []string{
                "Alice",
                "Bob",
                "Carol",
                "David",
            },
        })
        if err != nil {
            fmt.Fprintf(w, "Execute: %v", err)
            return
        }
    })

    log.Println("Starting HTTP server...")
    log.Fatal(http.ListenAndServe("localhost:4000", nil))
}

上例中的代码作用非常简单,即先通过 map[string]interface{} 类型的根对象传递一个名为 “Names” 的切片,该切片包含了四个人名。然后通过模板的 range 语句对这个切片进行迭代,依次输出每个人名。

值得注意的是,这里我们使用的语法结构为 range $name := .Names,其中 .Names 是被迭代的集合,而变量 $name 则是当次迭代中获取到的单个对象。在本例中,变量 $name 实际上为 string 类型。

尝试运行以上代码可以在终端获得以下结果:

➜ curl http://localhost:4000
Alice
Bob
Carol
David

range 语句除了可以获取到当次迭代的对象以外,还能够和 Go 语言源代码中一样,获取到一个当前迭代所对应的索引值。

...

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        // 创建模板对象并解析模板内容
        tmpl, err := template.New("test").Parse(`
{{range $i, $name := .Names}}
    {{$i}}. {{$name}}
{{end}}
`)

...

(为了缩减篇幅并更好地专注于有变动的部分,部分未改动的代码块使用了 “…” 进行替代。)

尝试运行以上代码可以在终端获得以下结果:

➜ curl http://localhost:4000
0. Alice
1. Bob
2. Carol
3. David

可以看到,通过使用语法结构 range $i, $name := .Names,我们可以再获取变量 $name 的同时获取变量 $i (索引)的值。

就模板语法而言,迭代不同类型的集合是没有区别的,我们可以来看一下如何在模板中对 map 类型的集合进行迭代操作:

...

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        // 创建模板对象并解析模板内容
        tmpl, err := template.New("test").Parse(`
{{range $name, $val := .}}
    {{$name}}: {{$val}}
{{end}}
`)

        ...

        // 调用模板对象的渲染方法
        err = tmpl.Execute(w, map[string]interface{}{
            "Names": []string{
                "Alice",
                "Bob",
                "Carol",
                "David",
            },
            "Numbers": []int{1, 3, 5, 7},
        })

        ...
}

(为了缩减篇幅并更好地专注于有变动的部分,部分未改动的代码块使用了 “…” 进行替代。)

尝试运行以上代码可以在终端获得以下结果:

➜ curl http://localhost:4000
Names: [Alice Bob Carol David]
Numbers: [1 3 5 7]

上例中,我们通过直接迭代作为根对象的 map,然后打印其中所包含的键值对。和迭代其它类型集合的唯一不同在于,语法结构 range $name, $val := . 获得到的第一个变量不再是索引,而是当次迭代所对应的键名。

在模板中使用语境操作(with 语句)

在学习如何使用语境操作(with 语句)之前,我们先来看一看下面的示例:

package main

import (
    "fmt"
    "log"
    "net/http"
    "text/template"
)

type Inventory struct {
    SKU       string
    Name      string
    UnitPrice float64
    Quantity  int64
}

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        // 创建模板对象并解析模板内容
        tmpl, err := template.New("test").Parse(`Inventory
SKU: {{.Inventory.SKU}}
Name: {{.Inventory.Name}}
UnitPrice: {{.Inventory.UnitPrice}}
Quantity: {{.Inventory.Quantity}}
`)
        if err != nil {
            fmt.Fprintf(w, "Parse: %v", err)
            return
        }

        // 调用模板对象的渲染方法
        err = tmpl.Execute(w, map[string]interface{}{
            "Inventory": Inventory{
                SKU:       "11000",
                Name:      "Phone",
                UnitPrice: 699.99,
                Quantity:  666,
            },
        })
        if err != nil {
            fmt.Fprintf(w, "Execute: %v", err)
            return
        }
    })

    log.Println("Starting HTTP server...")
    log.Fatal(http.ListenAndServe("localhost:4000", nil))
}

本例中,我们定义和创建了一个 Inventory 类型的对象,并将它放入对根对象中,关联键名为 “Inventory”。尝试运行以上代码可以在终端获得以下结果:

➜ curl http://localhost:4000
Inventory
SKU: 11000
Name: Phone
UnitPrice: 699.99
Quantity: 666

这里我们要关注的并不是程序的运行结果,而是模板的内容:

Inventory
SKU: {{.Inventory.SKU}}
Name: {{.Inventory.Name}}
UnitPrice: {{.Inventory.UnitPrice}}
Quantity: {{.Inventory.Quantity}}

不难发现,为了能够渲染 “Inventory” 的每一个值,我们都需要先通过点操作获取根对象中键名为 “Inventory” 的对象,然后再通过第二次点操作才能获取到具体某个字段的值。

在模板内容较少的情况下,这样的做法没有什么问题,但如果 “Inventory” 对象需要被使用非常多次数,或者甚至我们需要通过多次点操作才能获取到我们所要获得的值呢?例如:.Storage.Repository.Inventory。在这种情况下,模板的内容就会显得非常冗余。

为了解决这个问题,就可以使用语境操作(with 语句)啦!学习使用过 Visual Basic 的同学可能会对 with 语句的用法和作用比较熟悉。

...

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        // 创建模板对象并解析模板内容
        tmpl, err := template.New("test").Parse(`Inventory
{{with .Inventory}}
    SKU: {{.SKU}}
    Name: {{.Name}}
    UnitPrice: {{.UnitPrice}}
    Quantity: {{.Quantity}}
{{end}}
`)

...

(为了缩减篇幅并更好地专注于有变动的部分,部分未改动的代码块使用了 “…” 进行替代。)

尝试运行以上代码可以得到和之前一模一样的运行结果。

在使用了 with 语句之后,是不是觉得模板内容更加简洁易懂了呢?

模板中的空白符号处理

细心的你可能已经发现,在运行之前示例的时候,终端得到的响应实际上会带有多余的空行,例如:

➜ curl http://localhost:4000
Inventory

    SKU: 11000
    Name: Phone
    UnitPrice: 699.99
    Quantity: 666

这是因为我们在编写模板内容的时候,为了格式上的清晰加入了这些空行。如果我们想要更加整洁的输出结果的话,就可以使用 Go 语言标准库模板引擎的一个特殊语法,{{--}}

{{- 表示剔除模板内容左侧的所有空白符号,-}} 表示剔除模板内容右侧的所有空白符号。

...

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        // 创建模板对象并解析模板内容
        tmpl, err := template.New("test").Parse(`Inventory
{{- with .Inventory}}
    SKU: {{.SKU}}
    Name: {{.Name}}
    UnitPrice: {{.UnitPrice}}
    Quantity: {{.Quantity}}
{{- end}}
`)

...

(为了缩减篇幅并更好地专注于有变动的部分,部分未改动的代码块使用了 “…” 进行替代。)

这里需要特别注意减号两侧的空格,如果没有使用空格将减号与模板中其它内容分开的话,会被模板引擎误以为是表达式的一部分。例如,使用 {{-with .Inventory}} 则会报如下错误:

unexpected bad number syntax: "-w" in command

小结

这节课,我们主要学习了标准库中 text/template 包提供的文本模板引擎的逻辑控制、集合对象迭代和空白符号处理的用法。

下节课,我们将基于这节课所学的基础用法上,进一步学习如何在 Go 语言提供的模板引擎中使用自定义模板、模板函数和响应 HTML 内容。


原文地址:https://github.com/unknwon/building-web-applications-in-go/blob/master/articles/03.md


有疑问加站长微信联系(非本文作者))

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

3338 次点击  ∙  3 赞  
加入收藏 微博
被以下专栏收入,发现更多相似内容
6 回复  |  直到 2019-11-28 14:50:39
qnfnypen
qnfnypen · #1 · 5年之前

我使用{{$name := "Bob"}}这样的语法并没有出现什么呀?依然是正常输出和{{$name = "Bob"}}输出结果一致

Unknwon
Unknwon · #2 · 5年之前
qnfnypenqnfnypen #1 回复

我使用{{$name := "Bob"}}这样的语法并没有出现什么呀?依然是正常输出和{{$name = "Bob"}}输出结果一致

你针对的是哪一段内容?

qnfnypen
qnfnypen · #3 · 5年之前

针对的是在“模板中定义变量”那一小节,最后的“如果重复使用 := 的语法给相同名称的变量多次赋值会发生什么呢?”这里

Unknwon
Unknwon · #4 · 5年之前

@qnfnypen 哈哈 好的~ 老哥好认真!

l7l1l0l
l7l1l0l · #5 · 5年之前

多次使用:=来定义同一个变量并不会有什么问题,是否表明我全部使用:=来赋值或者定义就是没有问题的

Unknwon
Unknwon · #6 · 5年之前

@l7l1l0l 看了第四课你就知道什么时候会有区别啦

添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传