Go 标准库的 html/template 用久了就知道它的痛点:没有类型检查、重构困难、IDE 支持差。Templ 是一个新思路——在编译期把 .templ 文件生成为 Go 代码,提供完整的类型安全和 IDE 补全。最近在项目中试用了一段时间,体验确实好很多。
Templ vs html/template
| 对比项 | html/template | Templ |
|---|---|---|
| 类型安全 | 无,运行时才报错 | 编译期类型检查 |
| IDE 支持 | 基本没有 | LSP 支持,补全/跳转/重构 |
| 组件化 | 需要手动管理 partial | 原生组件系统 |
| 性能 | 运行时解析模板 | 编译为 Go 代码,零运行时开销 |
| 学习曲线 | Go 模板语法 | 类 JSX 语法 |
| 与 Go 集成 | 传 interface{} | 直接传 Go 类型 |
安装与基本使用
go install github.com/a-h/templ/cmd/templ@latest
创建第一个 .templ 文件 hello.templ:
package pages
templ Hello(name string) {
<div class="greeting">
<h1>Hello, { name }!</h1>
<p>Welcome to Templ.</p>
</div>
}
生成 Go 代码:
templ generate
这会生成 hello_templ.go,包含一个 func Hello(name string) templ.Component 函数。在 handler 中使用:
package main
import (
"net/http"
"myapp/pages"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
pages.Hello("World").Render(r.Context(), w)
})
http.ListenAndServe(":8080", nil)
}
组件化
Templ 的组件就是函数,可以自然地组合:
package components
// 布局组件,children 是插槽
templ Layout(title string) {
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8"/>
<title>{ title }</title>
<link rel="stylesheet" href="/static/style.css"/>
</head>
<body>
<nav>
@NavBar()
</nav>
<main>
{ children... }
</main>
<footer>
@Footer()
</footer>
</body>
</html>
}
templ NavBar() {
<div class="nav">
<a href="/">Home</a>
<a href="/about">About</a>
</div>
}
templ Footer() {
<div class="footer">
<p>© 2024</p>
</div>
}
页面组件使用布局:
package pages
import "myapp/components"
templ HomePage(posts []Post) {
@components.Layout("首页") {
<h1>最近文章</h1>
for _, post := range posts {
@PostCard(post)
}
}
}
templ PostCard(post Post) {
<article class="card">
<h2>{ post.Title }</h2>
<time>{ post.CreatedAt.Format("2006-01-02") }</time>
<p>{ post.Summary }</p>
</article>
}
注意 { children... } 语法——这是 Templ 的插槽机制,等价于 React 的 props.children。
条件与循环
templ UserProfile(user User) {
<div class="profile">
<h2>{ user.Name }</h2>
// 条件渲染
if user.IsAdmin {
<span class="badge">管理员</span>
} else {
<span class="badge">普通用户</span>
}
// 循环
if len(user.Tags) > 0 {
<ul>
for _, tag := range user.Tags {
<li>{ tag }</li>
}
</ul>
}
</div>
}
语法和 Go 完全一致,没有额外的模板语法需要学习。
与 htmx 配合
Templ + htmx 是当前 Go Web 开发一个非常香的组合——服务端渲染 + 局部更新,不需要写 JavaScript:
templ TodoList(todos []Todo) {
<div id="todo-list">
for _, todo := range todos {
@TodoItem(todo)
}
</div>
<form hx-post="/todos" hx-target="#todo-list" hx-swap="beforeend">
<input type="text" name="title" placeholder="新任务..." />
<button type="submit">添加</button>
</form>
}
templ TodoItem(todo Todo) {
<div class="todo-item" id={ fmt.Sprintf("todo-%d", todo.ID) }>
<input type="checkbox"
if todo.Done {
checked
}
hx-patch={ fmt.Sprintf("/todos/%d/toggle", todo.ID) }
hx-target={ fmt.Sprintf("#todo-%d", todo.ID) }
hx-swap="outerHTML"
/>
<span>{ todo.Title }</span>
</div>
}
htmx 请求后端返回 HTML 片段,Templ 负责渲染片段——职责分明,代码量小。
热重载
开发时用 templ generate --watch 监听文件变化:
# 终端 1:监听 .templ 文件变化,自动生成 Go 代码
templ generate --watch
# 终端 2:用 air 做 Go 代码的热重载
air
推荐配合 air 使用。.air.toml 中把 _templ.go 加入监听范围:
[build]
cmd = "go build -o ./tmp/main ."
include_ext = ["go", "templ"]
exclude_regex = ["_test.go"]
小结
Templ 在 Go 模板方案中是一个很实际的进步。类型安全和 IDE 支持解决了 html/template 的核心痛点,组件化和与 htmx 的配合让它在中小型 Web 项目中非常好用。缺点是多了一步代码生成,CI 配置要注意把 templ generate 加到构建流程里。