一、html/template 的暗坑,你踩过几个?
Go 标准库的 html/template 足以应付简单页面,但当项目膨胀到几十个模板文件、上百个数据字段时,它的设计缺陷会逐一暴露:
- 类型安全为零:传参靠 interface{},字段名拼错、类型不匹配全部推迟到运行时才发现。重构一个 struct 字段名,IDE 不会告诉你模板里哪处还在用旧名字。
- 组件复用靠复制粘贴:所谓的 "partial" 本质是字符串拼接,没有类型契约,传参全靠约定。
- 工具链几乎空白:没有格式化工具、没有 LSP 支持,模板文件在编辑器里就是彩色纯文本。
这些问题不是 Go 团队没有意识到,而是 html/template 的设计年代(2011 年)还不存在编译期模板类型检查这个范式。十年后,templ 补上了这块拼图。
二、templ 是什么
templ 是一个 Go 语言的 HTML 模板引擎,核心思路是把 .templ 文件在编译期生成为纯 Go 代码 ,从而让整个 Go 编译器和工具链参与类型检查。截至 2026 年 5 月,最新稳定版为 v0.3.1021,GitHub Star 数已突破 9k+。
它的语法类似 JSX------HTML 和 Go 代码写在同一个文件中,由 templ generate 命令生成对应的 _templ.go 文件。生成的代码直接渲染到 http.ResponseWriter,零运行时模板解析开销。
三、templ vs html/template:一张表说清差距
| 维度 | html/template | templ |
|---|
|--------|-----------------------------------|----------------------------------|
| 类型安全 | 无,运行时 panic 才发现字段名拼错 | 编译期检查,参数类型不对直接编译失败 |
| IDE 支持 | 基本没有(模板语法高亮勉强可用) | 完整 LSP:自动补全、跳转定义、重命名重构 |
| 组件化 | 手动管理 partial,传参靠约定 | 原生组件系统,@Component(args) 调用,参数强类型 |
| 性能 | 运行时解析模板树,有反射开销 | 编译为纯 Go 函数调用,零运行时解析 |
| 格式化 | 无官方工具 | templ fmt 自动格式化 |
| 学习成本 | 需要学一套模板 DSL({{range}}、{{with}} 等) | 直接写 Go 的 if/for/switch,0 额外语法 |
| 构建流程 | 无额外步骤 | 需要 templ generate 代码生成步骤 |
一句话总结:templ 把模板从「运行时字符串处理」升级为「编译期类型化组件」。
四、适用场景
4.1 Go + HTMX 全栈应用
这是 templ 最强势的场景。HTMX 让前端交互(点击、表单提交、无限滚动)通过 HTML 属性声明完成,服务端只需返回 HTML 片段。templ 负责渲染这些类型安全的片段,二者组合后前端一行 JavaScript 都不需要写。
实际案例:一个 Todo 应用的后端返回 <tr> 片段,HTMX 直接 swap 到 DOM 中,templ 保证每个 <tr> 的数据字段都是编译期校验过的。
4.2 中大型 Web 后台管理系统
后台系统页面多、表单多、数据模型复杂。html/template 在几十个页面后维护成本陡增------重构一个 User 结构体的字段名,你不知道哪些模板会炸。templ 让编译器帮你找。
4.3 邮件模板渲染
邮件 HTML 模板通常需要反复调试,任何一处数据字段拼写错误都可能导致邮件内容异常。templ 的编译期检查可以避免这类生产事故。
4.4 服务端渲染 (SSR) 页面
对于 SEO 敏感的内容型站点,templ 提供了原生 SSR 能力,不需要引入 Next.js 这类重型框架。
不适合的场景:纯 API 服务(不渲染 HTML)、前端已是 React/Vue SPA 且 SSR 由 Node.js 负责的项目。
五、快速上手:从安装到第一个组件
5.1 安装
bash
go install github.com/a-h/templ/cmd/templ@latest
确认安装成功:
bash
templ version
# v0.3.1021
5.2 项目初始化
bash
mkdir templ-demo && cd templ-demo
go mod init templ-demo
5.3 第一个组件
创建 components/hello.templ:
html
package components
templ Hello(name string) {
<div class="greeting">
<h1>Hello, { name }!</h1>
<p>Welcome to templ.</p>
</div>
}
核心语法点:
- templ Hello(name string) 声明一个组件,本质上就是一个 Go 函数,参数带类型。
- { name } 是表达式插值,自动进行 HTML 转义。
- HTML 标签直接写,没有额外的包裹语法。
5.4 生成 Go 代码
bash
templ generate
这条命令会在同目录下生成 hello_templ.go,内含 func Hello(name string) templ.Component。
5.5 在 HTTP handler 中使用
Go
package main
import (
"net/http"
"templ-demo/components"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
components.Hello("World").Render(r.Context(), w)
})
http.ListenAndServe(":8080", nil)
}
访问 http://localhost:8080,页面显示 "Hello, World!"。
六、组件化实战:布局、插槽与组合
6.1 布局组件
html
package components
templ Layout(title string) {
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8"/>
<title>{ title }</title>
</head>
<body>
<nav>@NavBar()</nav>
<main>{ children... }</main>
<footer>@Footer()</footer>
</body>
</html>
}
templ NavBar() {
<nav class="top-nav">
<a href="/">首页</a>
<a href="/posts">文章</a>
<a href="/about">关于</a>
</nav>
}
templ Footer() {
<footer class="site-footer">
<p>© 2026 templ-demo</p>
</footer>
}
{ children... } 是插槽语法,调用方可以在 Layout 标签体内填充任意内容。
6.2 页面组件使用布局
html
package pages
import "templ-demo/components"
templ HomePage(posts []Post) {
@components.Layout("首页") {
<section class="hero">
<h1>最新文章</h1>
</section>
<div class="post-list">
for _, post := range posts {
@PostCard(post)
}
</div>
}
}
templ PostCard(p Post) {
<article class="post-card">
<h2>{ p.Title }</h2>
<time datetime={ p.CreatedAt.Format("2006-01-02") }>
{ p.CreatedAt.Format("2006-01-02") }
</time>
<p>{ p.Summary }</p>
<a href={ "/posts/" + p.Slug }>阅读全文</a>
</article>
}
关键点:
- for _, post := range posts 直接写 Go 的 range 循环,不需要 {{range}} 这类模板语法。
- @PostCard(post) 组件调用带参数,Post 结构体字段如果有变化,编译器会告诉你哪些地方需要同步修改。
6.3 条件渲染
html
templ UserBadge(user User) {
<div class="user-badge">
<span class="name">{ user.Name }</span>
if user.IsAdmin {
<span class="tag tag-admin">管理员</span>
} else if user.IsVIP {
<span class="tag tag-vip">VIP</span>
}
</div>
}
if/else if/else 就是原生的 Go 条件语句,不需要学额外语法。
6.4 属性动态绑定
html
templ TaskRow(task Task) {
<tr id={ "task-" + strconv.Itoa(task.ID) }>
<td>
<input type="checkbox"
if task.Done { checked }
hx-patch={ "/tasks/" + strconv.Itoa(task.ID) + "/toggle" }
hx-target={ "#task-" + strconv.Itoa(task.ID) }
hx-swap="outerHTML"
/>
</td>
<td class={ taskClass(task) }>{ task.Title }</td>
</tr>
}
属性值用 { } 包裹即可动态计算;布尔属性如 checked 可以直接写在 if 块中,条件为真时渲染,为假时完全省略。
七、templ + HTMX:不写 JavaScript 的现代交互
这是 templ 社区最主流的用法。核心思路:
- 服务端用 templ 渲染 HTML 片段
- 前端用 HTMX 属性声明交互行为(请求方式、目标元素、替换策略)
- 后端 handler 返回 templ 组件渲染的 HTML 片段
以一个完整的 Todo 应用为例:
后端 handler:
Go
func handleToggleTodo(w http.ResponseWriter, r *http.Request) {
id, _ := strconv.Atoi(chi.URLParam(r, "id"))
task := db.ToggleTask(id) // 切换完成状态
components.TaskRow(task).Render(r.Context(), w) // 只返回一行 HTML
}
前端模板(templ 组件):
html
templ TaskList(tasks []Task) {
<div id="task-list" class="task-list">
for _, task := range tasks {
@TaskRow(task)
}
</div>
<form hx-post="/tasks" hx-target="#task-list" hx-swap="beforeend">
<input type="text" name="title" placeholder="新任务..." required/>
<button type="submit">添加</button>
</form>
}
HTMX 的 hx-post 触发 POST 请求,后端返回新行的 HTML 片段,hx-target 指定插入位置,hx-swap="beforeend" 表示追加到列表末尾。全程零 JavaScript,且 templ 保证了每一段 HTML 的类型安全。
八、开发体验:热重载与 LSP
8.1 热重载
开发时同时运行两个进程:
bash
# 终端 1:监听 .templ 文件变化,自动生成 Go 代码
templ generate --watch
# 终端 2:监听 Go 代码变化,自动重新编译运行
air
.air.toml 配置示例:
bash
[build]
cmd = "go build -o ./tmp/main ."
include_ext = ["go", "templ"]
exclude_regex = ["_test.go"]
修改 .templ 文件后,templ generate --watch 自动生成新的 _templ.go,air 检测到 Go 文件变化后自动重启服务。整个流程约 1-2 秒。
8.2 Editor 支持
templ 提供了官方 LSP 实现,支持 VS Code / Neovim / GoLand:
- 语法高亮:.templ 文件中的 HTML 和 Go 代码各自高亮
- 自动补全:组件名、函数参数均可自动补全
- 跳转定义:Ctrl+Click 跳转到组件定义
- 诊断提示:类型错误、未定义变量等在编辑器中实时标红
VS Code 安装方式:搜索 templ 扩展即可。
九、潜在坑点与规避
| 坑点 | 说明 | 解决方案 |
|---|
|------------|-------------------------------------|--------------------------------------------------|
| 代码生成步骤 | .templ 文件不直接参与编译,必须先 templ generate | CI 中加入 templ generate 步骤;或提交 _templ.go 到仓库(推荐前者) |
| 属性插值遗漏 | 动态属性值忘记加 { },渲染为字面文本 | templ fmt 不能检测此问题,需要人工 + LSP 检查 |
| 原始 HTML 注入 | 使用 templ.Raw 跳过转义时需谨慎 | 仅对可信内容使用 Raw,用户输入绝对不要用 |
| 并发渲染 | 多个 goroutine 同时调用同一组件的 Render | 确保组件内部不共享可变状态 |
十、总结
templ 不是对 html/template 的小修小补,而是一次范式升级------把模板从字符串处理的世界拉进类型系统的保护伞下。对于用 Go 做 Web 服务端渲染的团队,它可以显著降低:
- 运行时故障:字段名拼错、类型不匹配在编译期直接暴露
- 重构成本:改一个 struct 字段名,IDE 自动帮你在所有 .templ 文件中同步
- 新人上手成本:不需要学模板 DSL,会写 Go 的 if/for 就会写 templ
如果在选型 Go Web 模板方案,templ + HTMX 是目前社区验证过的、生产级别的组合。代码生成这一步带来的类型安全收益,远超它引入的构建流程成本。