单文件渲染
Gin 默认允许只使用一个 html 模板。c.HTML(),可以指定一个HTML文件。这个待渲染的模板中,通过{{ template "header.html" .}}来引用公用部分 适合页面数量较少的场景(3~5个)
# 文件结构如下,无需在模板文件中定义define,也不支持子文件夹
templates
├── about.html
├── footer.html
├── head.html
├── header.html
└── index.html
<!-- 公用部分header.html -->
<header>
This is the header
</header>
<!-- 引用公用部分 index.html -->
{{ template "head.html" .}}
{{ template "header.html" .}}
{{ template "aside.html" .}}
<main> <h1>content:{{ .title}}</h1></main>
{{ template "footer.html" .}}
</body>
</html>
多模板渲染
github.com/gin-contrib… 可以使用 go 1.6 block template 等功能。
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"weued/renderer"
)
func main() {
fmt.Println("Hello World!")
r := gin.Default()
r.HTMLRender = renderer.LoadTemplates("view")
r.GET("/", func(c *gin.Context) {
c.HTML(200, "index.html", gin.H{
"title": "Home Page",
})
})
r.Run(":9099")
}
package renderer
import (
"github.com/gin-contrib/multitemplate"
"path/filepath"
)
func LoadTemplates(templateDir string) multitemplate.Renderer {
r := multitemplate.NewRenderer()
layouts, err := filepath.Glob(filepath.Join(templateDir, "layouts", "*.html"))
if err != nil {
panic(err)
}
partials, err := filepath.Glob(filepath.Join(templateDir, "partials", "*.html"))
if err != nil {
panic(err)
}
pages, err := filepath.Glob(filepath.Join(templateDir, "pages", "*.html"))
if err != nil {
panic(err)
}
funcMap := FuncMap()
for _, page := range pages {
// 合并 layout + partials + page
files := append([]string{}, layouts...)
files = append(files, partials...)
files = append(files, page)
name := filepath.Base(page)
r.AddFromFilesFuncs(name, funcMap, files...)
}
return r
}
package renderer
import (
"fmt"
"html/template"
"regexp"
"strings"
"time"
)
// 正则匹配 URL
var (
urlRegex = regexp.MustCompile(`(https?://[^\s"'<>]+)`)
tagRegex = regexp.MustCompile(`<(\/?[^>]+)>`)
)
// FuncMap 提供模板可用的函数
func FuncMap() template.FuncMap {
return template.FuncMap{
"formatDate": func(t any) string {
// 支持 time.Time 或 string
switch v := t.(type) {
case string:
if len(v) >= 10 {
return v[:10] // 截取 YYYY-MM-DD
}
return v
case time.Time:
return v.Format("2006-01-02")
default:
return ""
}
},
"formatDateTime": func(t time.Time) string {
return t.Format("2006-01-02 15:04")
},
"nl2br": func(text string) template.HTML {
// 替换 \r\n 或 \n 为 <br>
s := strings.ReplaceAll(text, "\r\n", "<br>")
s = strings.ReplaceAll(s, "\n", "<br>")
return template.HTML(s) // 用 template.HTML 标记为安全 HTML
},
"linkify": func(text template.HTML) template.HTML {
s := string(text)
// 暂存所有HTML标签
var tags []string
s2 := tagRegex.ReplaceAllStringFunc(s, func(tag string) string {
tags = append(tags, tag)
return fmt.Sprintf("{{TAG%d}}", len(tags)-1)
})
// 替换文本中的url为<a>标签
s2 = urlRegex.ReplaceAllStringFunc(s2, func(url string) string {
return `<a href="` + url + `" target="_blank">` + url + `</a>`
})
// 还原HTML标签
for i, tag := range tags {
s2 = strings.ReplaceAll(s2, fmt.Sprintf("{{TAG%d}}", i), tag)
}
return template.HTML(s2)
},
}
}
view
├── layouts
│ └── base.html
├── pages
│ └── index.html
└── partials
└── footer.html
<!-- base.html-->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, minimum-scale=1.0,user-scalable=no" />
<title>title</title>
</head>
<body>
{{ template "content" . }}
<button class="btn btn-primary">base</button>
</body>
</html>
{{ define "content" }}
<h1 class="text-slate-500">Hello,world!</h1>
<button class="btn btn-primary">content</button>
{{ template "footer" . }} {{ end }}
{{define "footer"}}
<div class="footer">
<div class="content">
<p>
© 2025
<a href="https://weued.com">weued.com</a>. All rights reserved.
</p>
</div>
</div>
{{end}}
单文件生成静态HTML
// 生成静态 HTML 文件
func GenerateStaticHTML(file string,outputPath string,outputFile string,data any) error {
// 加载模板
tmpl, err := template.ParseFiles(file)
if err != nil {
return err
}
// 渲染到 buffer
var buf bytes.Buffer
err = tmpl.Execute(&buf, data)
if err != nil {
return err
}
// 写入静态文件
os.MkdirAll(outputPath, 0755)
return os.WriteFile(filepath.Join(outputPath, outputFile), buf.Bytes(), 0644)
}
多模板生成静态HTML
package main
import (
"bytes"
"fmt"
"html/template"
"os"
"path/filepath"
"try/renderer"
)
// RenderStaticPage 将指定模板渲染成静态 HTML 文件
func RenderStaticPage(templateDir, pageFile, outputPath string, data map[string]any) error {
// 1. 加载所有模板文件(添加 FuncMap)
tmpl, err := template.New("").Funcs(renderer.FuncMap()).ParseGlob(filepath.Join(templateDir, "**/*.html"))
if err != nil {
return fmt.Errorf("failed to parse templates: %w", err)
}
// 2. 查找 base.html 模板
base := tmpl.Lookup("base.html")
if base == nil {
return fmt.Errorf("base.html not found")
}
// 3. 克隆 base.html 模板
baseTmpl, err := base.Clone()
if err != nil {
return fmt.Errorf("failed to clone base template: %w", err)
}
// 4. 查找页面模板
pageTemplate := tmpl.Lookup(filepath.Base(pageFile))
if pageTemplate == nil {
return fmt.Errorf("page template %s not found", pageFile)
}
// 5. 将页面模板内容添加到 "content" 模板中
_, err = baseTmpl.AddParseTree("content", pageTemplate.Tree)
if err != nil {
return fmt.Errorf("failed to add page template: %w", err)
}
// 6. 渲染模板到 buffer
var buf bytes.Buffer
if err := baseTmpl.Execute(&buf, data); err != nil {
return fmt.Errorf("failed to execute template: %w", err)
}
// 7. 创建输出目录
if err := os.MkdirAll(filepath.Dir(outputPath), 0755); err != nil {
return fmt.Errorf("failed to create output directory: %w", err)
}
// 8. 一次性写入文件
if err := os.WriteFile(outputPath, buf.Bytes(), 0644); err != nil {
return fmt.Errorf("failed to write output file: %w", err)
}
return nil
}
func main() {
data := map[string]any{
"title": "首页",
"pageFile": "index.html",
}
err := RenderStaticPage("view", "index.html", "output/index.html", data)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
fmt.Println("静态页面生成完成: output/index.html")
}
相关方法函数签名
func New(name string) *Template
tmpl := template.New("") // 创建名为 空 的模板
- 返回一个新的空模板对象
*Template,这个对象还没有任何内容。这个模板对象本身是空的,需要通过.Parse()、.ParseFiles()或.ParseGlob()来填充内容
func ParseFiles(filenames ...string) (*Template, error)
tmpl, err := template.ParseFiles("template/index.html")
- 将文件内容解析成模板对象。每个模板的名称默认为文件的基础名(不含路径)。例如 "templates/index.html" 的模板名称是 "index.html"
- 文件的先后次序是有区别的 —— 尤其是你在用 Go 的模板继承(
{{ define }}/{{ template }}) 机制时。 - 最后一个文件(最后一个参数)将成为返回的“主模板(root template)”。
- 所有文件都会被解析并加入到同一个模板集合中;
- 但
ParseFiles()返回的模板对象,是最后一个文件对应的模板。
func ParseGlob(pattern string) (*Template, error)
tmpl, err := template.ParseGlob("templates/**/*.html")
根据给定的通配符(glob pattern)匹配所有模板文件,然后依次解析并组合成一个模板集合(*template.Template)。
func Must(t *Template, err error) *Template
tmpl := template.Must(template.ParseGlob("templates/**/*.html"))
用于简化模板初始化时的错误处理。
-
如果
err != nil→ 直接panic(err)(程序崩溃并打印错误); -
如果
err == nil→ 返回模板对象。
链式调用
tmpl := template.Must( template.New("root").ParseFiles("templates/layouts/base.html").ParseGlob("templates/pages/*.html"),
)
tmpl := template.Must(
template.New("root").ParseGlob("templates/**/*.html"),
)
func (t *Template) Execute(wr io.Writer, data any) error
err := tmpl.Execute(writer, data) //执行主模板(第一个解析的文件) err := tmpl.ExecuteTemplate(writer, "index.html", data) //执行指定名称的模板
解析后需要使用 Execute 或 ExecuteTemplate 来渲染
ExecuteTemplate(w io.Writer, name string, data any) error
err = tmpl.ExecuteTemplate(os.Stdout, "base.html", data)
渲染模板集合中指定名字的模板
允许你从同一个模板集合中选择任意子模板渲染
var buf bytes.Buffer
bytes.Buffer 位于标准库 bytes 包,是一个 可变大小的字节缓冲区(buffer),常用于:
- 临时存储字节数据
- 构建或拼接字符串/字节切片
- 作为
io.Reader或io.Writer使用
它的内部结构是一个可自动增长的字节切片 ([]byte),性能比每次拼接字符串更高,尤其是在大量拼接场景下。
| 方法 | 功能 |
|---|---|
Write(p []byte) | 向 Buffer 末尾写入字节,返回写入长度和错误 |
WriteString(s string) | 向 Buffer 写入字符串 |
Read(p []byte) | 从 Buffer 读取数据到 p |
Bytes() | 返回 Buffer 内部的字节切片(只读,不影响 Buffer) |
String() | 返回 Buffer 内部数据的字符串形式 |
Reset() | 清空 Buffer,容量保留 |
func WriteFile(name string, data []byte, perm FileMode) error
写入文件的便捷函数,一次性完成创建文件、写入内容、关闭文件的操作。
- 如果文件已存在,会被完全覆盖
- 如果文件不存在,会自动创建