Go语言的标准库提供了强大的模板系统,分为两个主要包:text/template
和html/template
。这两个包使用相同的接口和语法,但html/template
包增加了对HTML特定的安全功能,防止跨站脚本攻击(XSS)。在Web开发中,我们主要使用html/template
包。
模板系统允许我们将数据和表现分离,实现以下目标:
这个例子展示了模板使用的基本步骤:
template.New()
创建一个新模板.Parse()
方法解析模板字符串.Execute()
方法将数据应用到模板并输出结果在模板字符串中,{{.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 .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 .User }}
<p>Name: {{.Name}}</p>
<p>Email: {{.Email}}</p>
{{ end }}
with
语句还可以包含条件,只有当值存在(非零)时才执行块:
{{ with .Error }}
<div class="error">{{.}}</div>
{{ end }}
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 }}
也可以使用and
、or
和not
进行逻辑运算:
{{ 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>© {{ .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
用于定义一个模板,语法如下:
{{define "name"}}
...
{{end}}
template
用于引用一个模板,其中,name
是要包含的模板的名称,.
是传递给被包含模板的数据。如果不需要传递数据,可以使用nil
代替。
{{ template "name" . }}
block
定义一个可覆盖的模板块(Template Block),允许子模板通过同名 {{define "name"}}
覆盖内容。若未被覆盖,则执行块内默认内容。
例如父模板中定义:
{{block "content" .}}
<h1>Default Content</h1>
{{end}}
子模板可以进行覆盖
{{define "content"}}
<h1>New Content</h1>
{{end}}
在使用模板时,有几个重要的考虑因素:
base.html
、partials/header.html
等。template.Must()
简化错误处理,但要注意只在初始化时使用。