Golang的标准库html/template包提供了强大的模板功能,通过合理的设计可以实现类似其他语言中模板继承和组合的效果,有效减少重复代码,让页面结构更清晰。这种方式适合构建多页面的Web应用,统一公共头部、底部、侧边栏等通用结构。

模板继承的核心逻辑
模板继承的本质是先定义一个基础模板,包含页面的公共结构,同时预留可被子模板替换的占位块。子模板只需要定义需要替换的块内容,不需要重复编写公共部分。在html/template中,我们可以通过define定义模板块,通过template指令引用其他模板。
基础模板定义
首先创建一个基础模板base.html,包含HTML基本结构、公共头部和底部,中间预留内容块:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>{{template "title" .}}</title>
<style>
.header { background: #f5f5f5; padding: 20px; }
.footer { background: #333; color: #fff; padding: 20px; margin-top: 30px; }
.content { padding: 20px; }
</style>
</head>
<body>
<div class="header">
<h1>我的Web应用</h1>
<nav>
<a href="/">首页</a>
<a href="/about">关于</a>
</nav>
</div>
<div class="content">
{{template "content" .}}
</div>
<div class="footer">
<p>© 版权所有</p>
</div>
</body>
</html>
上面的基础模板中,我们使用了两个template指令:{{template "title" .}}和{{template "content" .}},这两个就是预留的占位块,后面的点表示传递当前模板数据给被引用的模板。
子模板继承实现
接下来创建子模板,比如首页模板index.html,通过define定义和占位块同名的模板内容,实现替换:
{{define "title"}}首页 - 我的Web应用{{end}}
{{define "content"}}
<h2>欢迎来到首页</h2>
<p>这是首页的专属内容,其他公共部分由基础模板提供。</p>
<ul>
<li>最新文章1</li>
<li>最新文章2</li>
</ul>
{{end}}
再创建一个关于页模板about.html:
{{define "title"}}关于我们 - 我的Web应用{{end}}
{{define "content"}}
<h2>关于我们</h2>
<p>这里是关于页面的专属内容,公共头部和底部不需要重复编写。</p>
{{end}}
模板组合的实现方式
模板组合是指将多个小的模板片段组合成一个完整的模板,适合拆分独立的组件,比如导航栏、卡片组件等,这些组件可以在多个页面中复用。
定义组件模板
创建一个组件模板components.html,定义两个通用组件:
{{define "nav"}}
<nav class="nav">
<a href="/">首页</a>
<a href="/user">用户中心</a>
<a href="/logout">退出登录</a>
</nav>
{{end}}
{{define "card"}}
<div class="card" style="border:1px solid #eee; padding:15px; margin:10px 0;">
<h3>{{.Title}}</h3>
<p>{{.Desc}}</p>
</div>
{{end}}
在模板中组合组件
我们可以在其他模板中通过template指令引用这些组件,比如在用户中心模板user.html中组合使用:
{{define "title"}}用户中心{{end}}
{{define "content"}}
<h2>用户中心</h2>
{{template "nav" .}}
<div class="user-info">
<p>用户名:{{.Username}}</p>
</div>
<h3>我的内容</h3>
{{template "card" .Article1}}
{{template "card" .Article2}}
{{end}}
Golang代码加载与渲染模板
定义好模板文件后,需要在Golang代码中加载所有模板,然后处理请求时渲染对应的子模板。下面是完整的服务端代码示例:
package main
import (
"html/template"
"log"
"net/http"
)
func main() {
// 加载所有模板文件,这里需要把基础模板、子模板、组件模板都加载进去
// 注意加载顺序不影响,template包会自动识别define定义的模板
tmpl, err := template.ParseGlob("templates/*.html")
if err != nil {
log.Fatal("加载模板失败:", err)
}
// 首页路由
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// 渲染index.html中define的模板,因为index.html的内容被define包裹,直接执行base模板即可
// 因为base模板中的占位块会引用index中定义的title和content
err := tmpl.ExecuteTemplate(w, "base.html", map[string]interface{}{
"Username": "测试用户",
})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
})
// 关于页路由
http.HandleFunc("/about", func(w http.ResponseWriter, r *http.Request) {
err := tmpl.ExecuteTemplate(w, "base.html", nil)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
})
// 用户中心路由
http.HandleFunc("/user", func(w http.ResponseWriter, r *http.Request) {
data := map[string]interface{}{
"Username": "测试用户",
"Article1": map[string]string{
"Title": "文章1",
"Desc": "这是第一篇文章的内容",
},
"Article2": map[string]string{
"Title": "文章2",
"Desc": "这是第二篇文章的内容",
},
}
err := tmpl.ExecuteTemplate(w, "base.html", data)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
})
log.Println("服务启动在 :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
上面的代码中,我们使用template.ParseGlob加载templates目录下的所有html模板文件,然后通过ExecuteTemplate执行base.html模板,因为base.html是基础模板,会自动引用子模板中定义的占位块内容,传递的数据也会在模板中生效。
注意事项
- 所有模板文件都需要被加载,哪怕是基础模板引用的子模板、组件模板,都要在ParseGlob的匹配范围内,否则会报模板不存在的错误。
- 占位块的名称要一一对应,基础模板中
{{template "xxx" .}}的xxx名称,必须和子模板中{{define "xxx"}}的名称完全一致。 - 如果组件模板需要接收数据,要在
template指令后传递对应的数据,比如{{template "card" .Article1}},把Article1的数据传给card组件。 - 模板文件中的HTML特殊字符如果要原样输出,需要使用
html/template的转义机制,避免XSS安全问题,不要手动关闭转义除非确认内容安全。
总结
通过html/template包的define和template指令,我们可以很方便地实现Golang Web开发中的模板继承和组合。模板继承适合处理页面的公共结构,减少重复代码;模板组合适合拆分通用组件,提升组件的复用性。合理的模板设计可以让Web项目的代码结构更清晰,后期维护更方便。实际开发中可以根据项目的页面复杂度,灵活组合使用这两种方式。