gohtml

package module
v0.0.0-...-47c60cb Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 13, 2026 License: MIT Imports: 14 Imported by: 0

README

GoHTML

Go Reference Go Report Card

一个强大且易用的 Go HTML 模板引擎,基于标准库 html/template,支持模板继承、热重载和丰富的内置函数。

特性

  • 🎯 模板继承 - 使用 {{ extends "parent.gohtml" }} 实现模板继承
  • 🔥 热重载 - 开发环境支持模板热重载,无需重启
  • 📦 embed.FS 支持 - 生产环境可将模板编译进二进制
  • 🧩 丰富的内置函数 - 字符串处理、日期格式化、逻辑判断等 20+ 函数
  • 🌐 HTTP 中间件 - 内置 HTTP 中间件,轻松集成到 Web 应用
  • 🎨 全局数据注入 - 支持在 context 中注入全局模板数据
  • 🔒 类型安全 - 基于标准库 html/template,自动转义防止 XSS
  • 0️⃣ 零依赖 - 仅依赖 Go 标准库

安装

go get github.com/hupeh/gohtml

快速开始

基础示例

创建模板文件 templates/index.gohtml:

<!DOCTYPE html>
<html>
<head>
    <title>{{ .Title }}</title>
</head>
<body>
    <h1>{{ .Message }}</h1>
    <p>当前时间: {{ now | datetimeFormat }}</p>
</body>
</html>

Go 代码:

package main

import (
    "fmt"
    "github.com/hupeh/gohtml"
)

func main() {
    // 创建引擎
    engine := gohtml.New()
    
    // 从目录加载模板
    err := engine.JoinDir("./templates")
    if err != nil {
        panic(err)
    }
    
    // 渲染模板
    result, err := engine.Render("index", map[string]any{
        "Title":   "欢迎",
        "Message": "Hello, GoHTML!",
    })
    if err != nil {
        panic(err)
    }
    
    fmt.Println(string(result))
}
模板继承

父模板 templates/layout.gohtml:

<!DOCTYPE html>
<html>
<head>
    <title>{{ block "title" . }}默认标题{{ end }}</title>
    <style>
        {{ block "style" . }}{{ end }}
    </style>
</head>
<body>
    <header>
        <h1>我的网站</h1>
    </header>
    
    <main>
        {{ block "content" . }}{{ end }}
    </main>
    
    <footer>
        <p>&copy; 2024 我的网站</p>
    </footer>
</body>
</html>

子模板 templates/home.gohtml:

{{ extends "layout.gohtml" }}

{{ define "title" }}首页 - 我的网站{{ end }}

{{ define "style" }}
body {
    font-family: Arial, sans-serif;
    margin: 0;
    padding: 20px;
}
{{ end }}

{{ define "content" }}
<h2>欢迎来到首页</h2>
<p>用户: {{ .Username }}</p>
<p>今天是 {{ now | dateFormat }}</p>
{{ end }}

Go 代码:

engine := gohtml.New()
engine.JoinDir("./templates")

result, _ := engine.Render("home", map[string]any{
    "Username": "张三",
})
使用 embed.FS (生产环境)
package main

import (
    "embed"
    "github.com/hupeh/gohtml"
)

//go:embed templates/*.gohtml
var templateFS embed.FS

func main() {
    engine := gohtml.New()
    
    // 从嵌入的文件系统加载模板
    err := engine.JoinFS(templateFS)
    if err != nil {
        panic(err)
    }
    
    result, _ := engine.Render("index", map[string]any{
        "Title": "生产环境",
    })
}
HTTP 中间件集成
package main

import (
    "net/http"
    "github.com/hupeh/gohtml"
)

func main() {
    engine := gohtml.New()
    engine.JoinDir("./templates")
    
    mux := http.NewServeMux()
    
    // 使用中间件注入引擎和全局数据
    handler := gohtml.Middleware(engine, map[string]any{
        "SiteName": "我的网站",
        "Version":  "1.0.0",
    })(mux)
    
    // 路由处理 - 方式 1:使用 Render 函数(推荐)
    mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        // Render 函数会自动从 context 获取引擎和全局数据
        result, err := gohtml.Render(r.Context(), "index", map[string]any{
            "Title":   "首页",
            "Content": "欢迎访问",
        })
        if err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }
        w.Header().Set("Content-Type", "text/html; charset=utf-8")
        w.Write(result)
    })
    
    // 路由处理 - 方式 2:手动获取引擎
    mux.HandleFunc("/about", func(w http.ResponseWriter, r *http.Request) {
        eng, _ := gohtml.FromContext(r.Context())
        data := gohtml.GetData(r.Context())
        data["Title"] = "关于我们"
        
        result, _ := eng.Render("about", data)
        w.Header().Set("Content-Type", "text/html; charset=utf-8")
        w.Write(result)
    })
    
    http.ListenAndServe(":8080", handler)
}

内置函数完整列表

GoHTML 提供了丰富的内置函数,包括 Go 标准库的函数和自定义扩展函数。

📋 函数来源说明
  • 🔵 标准库 - 来自 Go html/template 标准库,行为保持不变
  • 🟢 覆盖 - 覆盖了标准库的同名函数,行为已修改
  • 🟣 扩展 - GoHTML 新增的自定义函数

⚠️ 重要说明:自动转义与类型转换函数

GoHTML 基于 html/template,默认会自动转义所有输出以防止 XSS 攻击。

<!-- 自动转义示例 -->
{{ .Content }}
<!-- 如果 Content = "<script>alert('xss')</script>" -->
<!-- 输出: &lt;script&gt;alert(&#39;xss&#39;)&lt;/script&gt; -->
<!-- 浏览器显示为纯文本,不执行脚本 -->

当你需要渲染可信的 HTML 内容时(如富文本编辑器的输出),使用类型转换函数跳过转义:

<!-- 跳过转义示例 -->
{{ html .Content }}
<!-- 如果 Content = "<h2>标题</h2><p>段落</p>" -->
<!-- 输出: <h2>标题</h2><p>段落</p> -->
<!-- 浏览器正确渲染 HTML -->

安全提示

  • ✅ 对用户输入的内容:使用 {{ .UserInput }}(自动转义)
  • ✅ 对可信的 HTML 内容:使用 {{ html .TrustedContent }}(跳过转义)
  • ⚠️ 永远不要对未经验证的用户输入使用 html 函数,这会导致 XSS 漏洞

类型转换函数

用于标记可信内容,跳过自动转义:

函数 来源 说明 示例
html 🟢 覆盖 标记为安全的 HTML(标准库是转义,这里是跳过转义) {{ html "<b>粗体</b>" }}
css 🟣 扩展 标记为安全的 CSS {{ css "color: red;" }}
js 🟢 覆盖 标记为安全的 JavaScript(标准库是转义,这里是跳过转义) {{ js "alert('hi')" }}
url 🟣 扩展 标记为安全的 URL {{ url "https://example.com" }}
attr 🟣 扩展 标记为安全的 HTML 属性 {{ attr "data-id='123'" }}
字符串处理函数
函数 来源 说明 示例
upper 🟣 扩展 转换为大写 {{ "hello" | upper }} → HELLO
lower 🟣 扩展 转换为小写 {{ "WORLD" | lower }} → world
trim 🟣 扩展 去除两端空白 {{ " text " | trim }} → text
trimPrefix 🟣 扩展 去除前缀 {{ trimPrefix "Hello" "He" }} → llo
trimSuffix 🟣 扩展 去除后缀 {{ trimSuffix "Hello" "lo" }} → Hel
replace 🟣 扩展 替换字符串 {{ replace "foo bar" "bar" "baz" -1 }}
split 🟣 扩展 分割字符串 {{ split "a,b,c" "," }} → [a b c]
join 🟣 扩展 连接字符串数组 {{ join .array ", " }}
contains 🟣 扩展 检查是否包含 {{ contains "hello" "ll" }} → true
hasPrefix 🟣 扩展 检查前缀 {{ hasPrefix "hello" "he" }} → true
hasSuffix 🟣 扩展 检查后缀 {{ hasSuffix "hello" "lo" }} → true
print 🔵 标准库 格式化输出(同 fmt.Sprint) {{ print "a" "b" }} → ab
printf 🔵 标准库 格式化输出(同 fmt.Sprintf) {{ printf "%s-%d" "id" 123 }} → id-123
println 🔵 标准库 格式化输出带换行(同 fmt.Sprintln) {{ println "hello" }}
len 🔵 标准库 返回长度 {{ len .Array }}, {{ len .String }}
index 🔵 标准库 访问数组/切片/映射元素 {{ index .Array 0 }}, {{ index .Map "key" }}
slice 🔵 标准库 切片操作 {{ slice .Array 1 3 }}
日期时间函数
函数 来源 说明 示例
now 🟣 扩展 获取当前时间 {{ now }}
formatTime 🟣 扩展 自定义格式化 {{ formatTime .time "2006-01-02" }}
dateFormat 🟣 扩展 格式化为日期 {{ now | dateFormat }} → 2024-01-13
datetimeFormat 🟣 扩展 格式化为日期时间 {{ now | datetimeFormat }} → 2024-01-13 15:04:05
rfc3339 🟣 扩展 RFC3339 格式化 {{ now | rfc3339 }} → 2024-01-13T15:04:05Z07:00
逻辑与比较函数
函数 来源 说明 示例
not 🔵 标准库 逻辑非 {{ if not .IsEmpty }}有内容{{ end }}
and 🔵 标准库 逻辑与 {{ if and .A .B }}都为真{{ end }}
or 🔵 标准库 逻辑或 {{ if or .A .B }}至少一个为真{{ end }}
eq 🔵 标准库 等于 (==) {{ if eq .Status "active" }}激活{{ end }}
ne 🔵 标准库 不等于 (!=) {{ if ne .Count 0 }}非零{{ end }}
lt 🔵 标准库 小于 (<) {{ if lt .Age 18 }}未成年{{ end }}
le 🔵 标准库 小于等于 (<=) {{ if le .Score 60 }}不及格{{ end }}
gt 🔵 标准库 大于 (>) {{ if gt .Price 100 }}贵{{ end }}
ge 🔵 标准库 大于等于 (>=) {{ if ge .Age 18 }}成年{{ end }}
default 🟣 扩展 提供默认值 {{ default "未知" .Name }}
其他函数
函数 来源 说明 示例
urlquery 🔵 标准库 URL 查询参数转义 {{ urlquery "a b" }} → a+b
call 🔵 标准库 调用函数 {{ call .Method .Arg }}

完整示例

博客应用示例

布局模板 templates/layout.gohtml:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{{ block "title" . }}{{ .SiteName }}{{ end }}</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: Arial, sans-serif; line-height: 1.6; }
        header { background: #333; color: #fff; padding: 1rem; }
        nav a { color: #fff; margin-right: 1rem; text-decoration: none; }
        main { max-width: 1200px; margin: 2rem auto; padding: 0 1rem; }
        footer { background: #f4f4f4; padding: 1rem; text-align: center; margin-top: 2rem; }
        {{ block "style" . }}{{ end }}
    </style>
</head>
<body>
    <header>
        <h1>{{ .SiteName }}</h1>
        <nav>
            <a href="/">首页</a>
            <a href="/about">关于</a>
            <a href="/contact">联系</a>
        </nav>
    </header>
    
    <main>
        {{ block "content" . }}{{ end }}
    </main>
    
    <footer>
        <p>&copy; {{ now | formatTime "2006" }} {{ .SiteName }} | 版本 {{ .Version }}</p>
    </footer>
</body>
</html>

文章列表 templates/posts.gohtml:

{{ extends "layout.gohtml" }}

{{ define "title" }}文章列表 - {{ .SiteName }}{{ end }}

{{ define "content" }}
<h2>最新文章</h2>

{{ if .Posts }}
    {{ range .Posts }}
    <article style="border-bottom: 1px solid #eee; padding: 1rem 0;">
        <h3><a href="/posts/{{ .ID }}">{{ .Title }}</a></h3>
        <p>{{ .Summary }}</p>
        <small>
            作者: {{ .Author | upper }} | 
            发布时间: {{ .PublishedAt | datetimeFormat }}
        </small>
    </article>
    {{ end }}
{{ else }}
    <p>暂无文章</p>
{{ end }}
{{ end }}

文章详情 templates/post.gohtml:

{{ extends "layout.gohtml" }}

{{ define "title" }}{{ .Post.Title }} - {{ .SiteName }}{{ end }}

{{ define "content" }}
<article>
    <h2>{{ .Post.Title }}</h2>
    <p>
        <small>
            作者: {{ .Post.Author }} | 
            发布: {{ .Post.PublishedAt | datetimeFormat }} |
            标签: {{ join .Post.Tags ", " }}
        </small>
    </p>
    
    <div style="margin-top: 2rem;">
        {{ html .Post.Content }}
    </div>
    
    {{ if .Post.UpdatedAt }}
    <p style="margin-top: 2rem; color: #666;">
        最后更新: {{ .Post.UpdatedAt | datetimeFormat }}
    </p>
    {{ end }}
</article>

<section style="margin-top: 3rem;">
    <h3>相关文章</h3>
    <ul>
        {{ range .RelatedPosts }}
        <li><a href="/posts/{{ .ID }}">{{ .Title }}</a></li>
        {{ else }}
        <li>暂无相关文章</li>
        {{ end }}
    </ul>
</section>
{{ end }}

API 文档

Engine
type Engine struct {
    // 私有字段
}

// 创建新的引擎实例
func New() *Engine

// 从 fs.FS 加载模板(支持 embed.FS)
func (e *Engine) JoinFS(fsys fs.FS) error

// 从目录加载模板(支持热重载)
func (e *Engine) JoinDir(dir string) error

// 渲染模板,返回字节数组
func (e *Engine) Render(name string, data any) ([]byte, error)
Template
type Template struct {
    // 私有字段
}

// 创建新的模板实例
func NewTemplate() *Template

// 设置模板分隔符
func (x *Template) Delims(left, right string) *Template

// 注册自定义函数
func (x *Template) Funcs(funcMap template.FuncMap) *Template

// 查找模板
func (x *Template) Lookup(name string) *template.Template

// 执行模板
func (x *Template) ExecuteTemplate(wr io.Writer, name string, data any) error

// 从目录加载模板
func (x *Template) ParseDir(root string, extensions []string) error

// 从 fs.FS 加载模板
func (x *Template) ParseFS(root fs.FS, extensions []string) error
Context 函数
// 将引擎存入 context
func WithEngine(ctx context.Context, engine *Engine) context.Context

// 从 context 获取引擎
func FromContext(ctx context.Context) (*Engine, bool)

// 存入单个数据
func WithDatum(ctx context.Context, key string, value any) context.Context

// 存入多个数据
func WithData(ctx context.Context, dataMap map[string]any) context.Context

// 获取所有数据
func GetData(ctx context.Context) map[string]any

// 从 context 中获取引擎并渲染模板(自动合并全局数据)
func Render(ctx context.Context, name string, data map[string]any) ([]byte, error)
HTTP 中间件
// 创建 HTTP 中间件
func Middleware(engine *Engine, globals ...map[string]any) func(next http.Handler) http.Handler

高级用法

自定义函数
engine := gohtml.New()

// 注册自定义函数(需在加载模板前)
tmpl := gohtml.NewTemplate()
tmpl.Funcs(template.FuncMap{
    "currency": func(amount float64) string {
        return fmt.Sprintf("¥%.2f", amount)
    },
    "truncate": func(s string, length int) string {
        if len(s) <= length {
            return s
        }
        return s[:length] + "..."
    },
})

// 使用自定义模板
engine.t = tmpl
engine.JoinDir("./templates")

在模板中使用:

<p>价格: {{ currency 99.99 }}</p>
<p>摘要: {{ truncate .Content 100 }}</p>
自定义分隔符
tmpl := gohtml.NewTemplate()
tmpl.Delims("[[", "]]")  // 使用 [[ ]] 替代 {{ }}
嵌套模板
<!-- components/button.gohtml -->
{{ define "button" }}
<button class="{{ .Class }}" type="{{ default "button" .Type }}">
    {{ .Text }}
</button>
{{ end }}

<!-- page.gohtml -->
{{ template "button" dict "Class" "primary" "Text" "提交" "Type" "submit" }}

性能优化

生产环境最佳实践
  1. 使用 embed.FS: 将模板编译进二进制,避免磁盘 I/O
  2. 模板缓存: Engine 会自动缓存已解析的模板
  3. 避免频繁重载: 生产环境不要使用 JoinDir,优先使用 JoinFS
//go:build !dev

package main

import (
    "embed"
    "github.com/hupeh/gohtml"
)

//go:embed templates/*.gohtml
var templateFS embed.FS

func newEngine() *gohtml.Engine {
    engine := gohtml.New()
    engine.JoinFS(templateFS)
    return engine
}
开发环境热重载
//go:build dev

package main

import "github.com/hupeh/gohtml"

func newEngine() *gohtml.Engine {
    engine := gohtml.New()
    engine.JoinDir("./templates")
    return engine
}

构建命令:

# 开发环境
go build -tags dev

# 生产环境
go build

测试

运行测试:

go test ./...

运行带覆盖率的测试:

go test -cover ./...

许可证

MIT License - 详见 LICENSE

贡献

欢迎提交 Issue 和 Pull Request!

作者

hupeh

致谢

本项目基于 Go 标准库 html/template 构建,灵感来自 Django 和 Jinja2 模板引擎。

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func GetData

func GetData(ctx context.Context) map[string]any

GetData 从 context 中获取所有模板数据 返回注入的全局模板数据的拷贝,如果没有则返回空 map

func Middleware

func Middleware(engine *Engine, globals ...map[string]any) func(next http.Handler) http.Handler

Middleware 返回一个 HTTP 中间件,将 Engine 注入到请求的 context 中 这样在请求处理器中就可以通过 FromContext 获取模板引擎来渲染模板

参数:

  • engine: 模板引擎实例,会被注入到所有请求的 context 中
  • globals: 可选的全局模板数据,会自动合并到所有模板渲染中 可用于注入通用数据如站点名称、用户信息等

用法:

// 基础用法
mux.Use(gohtml.Middleware(engine))

// 带全局数据
mux.Use(gohtml.Middleware(engine, map[string]any{
    "siteName": "My Site",
    "version": "1.0.0",
}))

// 在处理器中使用
func handler(w http.ResponseWriter, r *http.Request) {
    engine, _ := gohtml.FromContext(r.Context())
    engine.Render(w, r.Context(), "page.gohtml", map[string]any{
        "title": "Home",
    })
}

func Render

func Render(ctx context.Context, name string, data map[string]any) ([]byte, error)

Render 从 context 中获取引擎并渲染模板 此函数会自动合并 context 中的全局数据和传入的 data

参数:

  • ctx: context.Context,必须包含通过 WithEngine 注入的引擎
  • name: 模板名称
  • data: 页面特定的数据,会覆盖 context 中的同名键

示例:

result, err := gohtml.Render(r.Context(), "index", map[string]any{
	"Title": "首页",
})

func WithData

func WithData(ctx context.Context, dataMap map[string]any) context.Context

WithData 将多个模板数据存储到 context 中 用于在中间件中批量注入全局模板数据,这些数据会自动合并到所有模板渲染中

func WithDatum

func WithDatum(ctx context.Context, key string, value any) context.Context

WithDatum 将模板数据存储到 context 中 用于在中间件中注入全局模板数据,这些数据会自动合并到所有模板渲染中

func WithEngine

func WithEngine(ctx context.Context, engine *Engine) context.Context

WithEngine 将 Engine 存储到 context 中 返回包含 Engine 的新 context

Types

type Engine

type Engine struct {
	// contains filtered or unexported fields
}

Engine 是模板引擎的主要接口 封装了 Template,提供简单易用的 API 来加载和渲染模板

func FromContext

func FromContext(ctx context.Context) (*Engine, bool)

FromContext 从 context 中获取 Engine 如果 context 中没有 Engine,返回 nil 和 false

func New

func New() *Engine

New 创建一个新的 Engine 实例 返回的 Engine 包含一个空的模板集合,需要通过 JoinFS 或 JoinDir 加载模板

func (*Engine) JoinDir

func (e *Engine) JoinDir(dir string) error

JoinDir 从指定目录加载所有 .gohtml 模板文件 dir 是目录的绝对路径或相对路径 这对于开发环境的热重载非常有用

示例:

engine := gohtml.New()
err := engine.JoinDir("./templates")

func (*Engine) JoinFS

func (e *Engine) JoinFS(fsys fs.FS) error

JoinFS 从 fs.FS 文件系统加载所有 .gohtml 模板文件 fsys 可以是 embed.FS、os.DirFS 或任何实现了 fs.FS 接口的类型

示例:

//go:embed *.gohtml
var templateFS embed.FS

engine := gohtml.New()
err := engine.JoinFS(templateFS)

func (*Engine) Render

func (e *Engine) Render(name string, data any) ([]byte, error)

Render 渲染指定的模板,返回渲染后的字节数组 name 是模板名称,可以不包含 .gohtml 扩展名(会自动添加) data 是传递给模板的数据,通常是 map[string]any 或结构体

示例:

result, err := engine.Render("index", map[string]any{
	"Title": "欢迎",
	"User":  user,
})
if err != nil {
	return err
}
w.Write(result)

type Template

type Template struct {
	// contains filtered or unexported fields
}

Template 是支持模板继承的核心类型 它管理所有模板及其继承关系,支持 {{ extends "parent.gohtml" }} 语法

func NewTemplate

func NewTemplate() *Template

NewTemplate 创建一个新的 Template 实例 返回的 Template 可以用来解析和执行模板

func (*Template) Delims

func (x *Template) Delims(left, right string) *Template

Delims 设置模板的分隔符 默认是 {{ 和 }},可以改为其他字符,如 [[ 和 ]] 返回 Template 自身以支持链式调用

func (*Template) ExecuteTemplate

func (x *Template) ExecuteTemplate(wr io.Writer, name string, data any) error

ExecuteTemplate 执行指定名称的模板,将结果写入 wr name 是模板文件名,data 是传递给模板的数据

func (*Template) Funcs

func (x *Template) Funcs(funcMap template.FuncMap) *Template

Funcs 注册自定义函数到模板中 这些函数可以在模板中直接调用 返回 Template 自身以支持链式调用

示例:

tmpl.Funcs(template.FuncMap{
	"upper": strings.ToUpper,
	"add": func(a, b int) int { return a + b },
})

func (*Template) Lookup

func (x *Template) Lookup(name string) *template.Template

Lookup 根据名称查找模板 返回 nil 表示模板不存在

func (*Template) ParseDir

func (x *Template) ParseDir(root string, extensions []string) error

ParseDir 从指定目录递归加载所有符合扩展名的模板文件 root 是目录路径,extensions 是允许的文件扩展名列表(如 [".gohtml"]) 目录路径会被规范化为使用正斜杠的格式

func (*Template) ParseFS

func (x *Template) ParseFS(root fs.FS, extensions []string) error

ParseFS 从 fs.FS 文件系统加载所有符合扩展名的模板文件 这是模板加载的核心方法,支持模板继承

工作流程: 1. 遍历文件系统,找到所有匹配扩展名的文件 2. 解析每个文件,识别是否有 extends 声明 3. 先解析所有非子模板到共享命名空间 4. 再解析子模板,将它们与父模板合并

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL