3、template

Go语言的标准库提供了强大的模板系统,分为两个主要包:text/templatehtml/template。这两个包使用相同的接口和语法,但html/template包增加了对HTML特定的安全功能,防止跨站脚本攻击(XSS)。在Web开发中,我们主要使用html/template包。

模板系统

模板系统概述

模板系统允许我们将数据和表现分离,实现以下目标:

基本使用

这个例子展示了模板使用的基本步骤:

在模板字符串中,{{.Name}}是一个特殊的标记,它会被替换为数据中的Name字段。

package main

import (
    "html/template"
    "net/http"
    "log"
)

func main() {
    // 定义处理函数
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        // 创建模板
        tmpl, err := template.New("hello").Parse("<h1>Hello, {{.Name}}!</h1>")
        if err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }
        
        // 准备数据
        data := struct {
            Name string
        }{
            Name: "Gopher",
        }
        
        // 渲染模板
        err = tmpl.Execute(w, data)
        if err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
        }
    })
    
    log.Println("Server started at :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

从文件加载模板

在实际应用中,我们通常不会将模板内容硬编码到程序中,而是从文件中加载。

模板文件 templates/index.html 的内容如下:

<!DOCTYPE html>
<html>
<head>
    <title>{{.Title}}</title>
</head>
<body>
    <h1>{{.Title}}</h1>
    <p>{{.Message}}</p>
    
    <ul>
        {{range .Items}}
            <li>{{.}}</li>
        {{end}}
    </ul>
</body>
</html>

搭建web服务,并解析模板。

使用template.ParseFiles()可以加载一个或多个模板文件。如果加载多个文件,它将返回一个模板集合,默认使用第一个文件作为主模板。

package main

import (
    "html/template"
    "net/http"
    "log"
)

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        // 从文件加载模板
        tmpl, err := template.ParseFiles("templates/index.html")
        if err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }
        
        // 准备数据
        data := struct {
            Title   string
            Message string
        }{
            Title:   "Go Templates",
            Message: "Welcome to Go Web Development!",
        }
        
        // 渲染模板
        err = tmpl.Execute(w, data)
        if err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
        }
    })
    
    log.Println("Server started at :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

模板缓存与预加载

在生产环境中,重复解析模板文件会造成不必要的性能开销。一种常见的优化是在应用启动时预加载所有模板。

使用template.ParseGlob()可以一次性加载多个模板文件,然后使用ExecuteTemplate()方法执行特定的模板。这种方式不仅提高了性能,还使代码更加简洁和可维护。

package main

import (
    "html/template"
    "net/http"
    "log"
)

// 全局模板集合
var templates *template.Template

func init() {
    // 预加载所有模板
    var err error
    templates, err = template.ParseGlob("templates/*.html")
    if err != nil {
        log.Fatal("Failed to parse templates:", err)
    }
}

func renderTemplate(w http.ResponseWriter, name string, data interface{}) {
    // 通过名称执行模板
    err := templates.ExecuteTemplate(w, name, data)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
    }
}

func homeHandler(w http.ResponseWriter, r *http.Request) {
    data := struct {
        Title   string
        Message string
    }{
        Title:   "Home Page",
        Message: "Welcome to our website!",
    }
    renderTemplate(w, "index.html", data)
}

func aboutHandler(w http.ResponseWriter, r *http.Request) {
    data := struct {
        Title string
        About string
    }{
        Title: "About Us",
        About: "We are a team of Go enthusiasts.",
    }
    renderTemplate(w, "about.html", data)
}

func main() {
    http.HandleFunc("/", homeHandler)
    http.HandleFunc("/about", aboutHandler)
    
    log.Println("Server started at :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

模板语法与数据操作

基本语法

Go模板使用双花括号{{ }}作为分隔符,其中可以包含变量、表达式、控制结构等。

变量和字段访问

{{.}} - 当前对象
{{.Field}} - 访问结构体字段
{{.Method}} - 调用方法(返回一个值)
{{.Field1.Field2}} - 嵌套字段访问
{{.Field.Method}} - 字段方法调用

例如,对于以下数据结构:

type User struct {
    Name  string
    Email string
    Age   int
}

func (u User) IsAdult() bool {
    return u.Age >= 18
}

data := struct {
    User  User
    Title string
}{
    User: User{
        Name:  "John Doe",
        Email: "john@example.com",
        Age:   25,
    },
    Title: "User Profile",
}

可以在模板中这样访问:

<h1>{{.Title}}</h1>
<p>Name: {{.User.Name}}</p>
<p>Email: {{.User.Email}}</p>
<p>Age: {{.User.Age}}</p>
<p>Is Adult: {{.User.IsAdult}}</p>

注释

在模板中添加注释:

{{/* 这是一个注释,不会输出到结果中 */}}

管道操作符

管道操作符|类似于Unix管道,将前一个命令的输出作为后一个命令的输入:

{{ .Name | printf "Name: %s" }}

这相当于printf("Name: %s",Name)

控制结构

if-else

{{ if .Condition }}
    <!-- 当.Condition为真时显示 -->
{{ else }}
    <!-- 当.Condition为假时显示 -->
{{ end }}

例如:

{{ if .User.IsAdult }}
    <p>User is an adult.</p>
{{ else }}
    <p>User is not an adult.</p>
{{ end }}

with

也可以使用with来简化嵌套字段的访问:

{{ with .User }}
    <p>Name: {{.Name}}</p>
    <p>Email: {{.Email}}</p>
{{ end }}

with语句还可以包含条件,只有当值存在(非零)时才执行块:

{{ with .Error }}
    <div class="error">{{.}}</div>
{{ end }}

range

range用于迭代切片、数组、映射或通道:

{{ range .Items }}
    <!-- 每个元素都可以通过.访问 -->
    <p>{{.}}</p>
{{ end }}

如果要访问索引:

{{ range $index, $element := .Items }}
    <p>{{$index}}: {{$element}}</p>
{{ end }}

对于映射:

{{ range $key, $value := .Map }}
    <p>{{$key}}: {{$value}}</p>
{{ end }}

range也可以包含else子句,当集合为空时执行:

{{ range .Items }}
    <p>{{.}}</p>
{{ else }}
    <p>No items found.</p>
{{ end }}

变量与作用域

可以使用$符号定义局部变量:

{{ $name := .Name }}
{{ $name }}

变量的作用域限于定义它的块内:

{{ $title := .Title }}
<h1>{{$title}}</h1>

{{ with .User }}
    {{ $username := .Name }}
    <p>Username: {{$username}}</p>
    <!-- $title仍然可用 -->
    <p>Page: {{$title}}</p>
{{ end }}

<!-- $username在此作用域不可用 -->

内置函数

Go模板系统提供了许多内置函数来操作数据:

{{len .Items}} - 返回切片、数组、映射或字符串的长度
{{index .Items 0}} - 访问数组、切片或映射中的元素
{{printf "格式字符串" .Value}} - 格式化输出
{{html .HTML}} - HTML转义(在html/template中自动应用)
{{js .Script}} - JavaScript转义
{{urlquery .URL}} - URL查询转义

一些实用的字符串函数:

{{lower .Title}} - 转换为小写
{{upper .Title}} - 转换为大写
{{trim .Content}} - 删除首尾空白

比较操作

Go模板支持基本的比较操作:

{{eq .A .B}} - 等于
{{ne .A .B}} - 不等于
{{lt .A .B}} - 小于
{{le .A .B}} - 小于等于
{{gt .A .B}} - 大于
{{ge .A .B}} - 大于等于

这些可以在if语句中使用:

{{ if eq .User.Role "admin" }}
    <p>Welcome, Administrator!</p>
{{ else }}
    <p>Welcome, User!</p>
{{ end }}

也可以使用andornot进行逻辑运算:

{{ if and (eq .User.Role "admin") (not .System.Maintenance) }}
    <p>Administrative actions are available.</p>
{{ end }}

自定义函数

除了内置函数,我们还可以定义自定义函数并将其添加到模板中:

package main

import (
	"html/template"
	"net/http"
	"strings"
	"time"
)

func main() {
	// 创建自定义函数映射
	funcMap := template.FuncMap{
		"formatDate": func(t time.Time) string {
			return t.Format("2006-01-02")
		},
		"capitalize": func(s string) string {
			return strings.Title(s)
		},
		"add": func(a, b int) int {
			return a + b
		},
	}

	// 创建带自定义函数的模板
	tmpl, err := template.New("page.html").
		Funcs(funcMap).
		ParseFiles("templates/page.html")
	if err != nil {
		panic(err)
	}

	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		data := struct {
			Name      string
			CreatedAt time.Time
			Count     int
		}{
			Name:      "example page",
			CreatedAt: time.Now(),
			Count:     5,
		}

		tmpl.Execute(w, data)
	})

	http.ListenAndServe(":8080", nil)
}

在模板中使用自定义函数:

<h1>{{ .Name | capitalize }}</h1>
<p>Created on: {{ formatDate .CreatedAt }}</p>
<p>Count plus ten: {{ add .Count 10 }}</p>

模板布局、嵌套和复用

在实际Web应用开发中,页面通常共享相同的布局(如页眉、页脚和导航栏)。Go模板系统提供了几种方式来实现代码复用和模板组合。

简单示例

base.html(基础布局):

<!DOCTYPE html>
<html>
<head>
    <title>{{ .Title }}</title>
    <link rel="stylesheet" href="/static/css/main.css">
</head>
<body>
    <header>
        {{ template "header" . }}
    </header>
    
    <main>
        {{ template "content" . }}
    </main>
    
    <footer>
        {{ template "footer" . }}
    </footer>
</body>
</html>

header.html(头部模板):

{{ define "header" }}
<nav>
    <ul>
        <li><a href="/">Home</a></li>
        <li><a href="/about">About</a></li>
        <li><a href="/contact">Contact</a></li>
    </ul>
</nav>
{{ end }}

footer.html(底部模板):

{{ define "footer" }}
<p>&copy; {{ .Year }} My Website. All rights reserved.</p>
{{ end }}

home.html(首页内容):

{{ define "content" }}
<h1>Welcome to {{ .Title }}</h1>
<p>{{ .Message }}</p>
{{ end }}

搭建 HTTP 服务器,渲染模板:

func main() {
    // 加载所有模板
    templates := template.Must(template.ParseFiles(
        "templates/base.html",
        "templates/header.html",
        "templates/footer.html",
        "templates/home.html",
    ))
    
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        data := struct {
            Title   string
            Message string
            Year    int
        }{
            Title:   "My Website",
            Message: "Welcome to our awesome website!",
            Year:    time.Now().Year(),
        }
        
        templates.ExecuteTemplate(w, "base.html", data)
    })
    
    http.ListenAndServe(":8080", nil)
}

define 和 template

define 用于定义一个模板,语法如下:

{{define "name"}}
...
{{end}}

template用于引用一个模板,其中,name是要包含的模板的名称,.是传递给被包含模板的数据。如果不需要传递数据,可以使用nil代替。

{{ template "name" . }}

block

block定义一个可覆盖的模板块(Template Block),允许子模板通过同名 {{define "name"}} 覆盖内容。若未被覆盖,则执行块内默认内容。

例如父模板中定义:

{{block "content" .}}
  <h1>Default Content</h1>
{{end}}

子模板可以进行覆盖

{{define "content"}}
  <h1>New Content</h1>
{{end}}

性能和组织考虑

在使用模板时,有几个重要的考虑因素: