Golang模板渲染方案及替代方案讨论

Golang模板渲染方案及替代方案讨论 我们来讨论一下模板库,在多重循环中使用 {{range}} 会因 HTML 转义和渲染到模板而变慢。在使用 html/template 渲染之前,是否有更好的方法一次性转义 Posts

Forum.go

import("html/template")

var tmpl = template.Must(template.ParseFiles("index.html"))

type Thread struct {
	Title   string
}

type ListThreads struct {
    Threads []*Thread
}

Posts := make([]*Thread, 30)
p := ListThreads{Posts}
tmpl.Execute(w, &p)

Template.html

{{ range .Threads }}
{{ .Title }}
{{ end }}

我也在考虑 Jet Template,它支持快速即时渲染以提高速度,值得一试吗?


更多关于Golang模板渲染方案及替代方案讨论的实战教程也可以访问 https://www.itying.com/category-94-b0.html

2 回复

silvero:

在使用 html/template 渲染之前,有没有更好的方法可以一次性转义 Posts

我发现 CSR(客户端渲染)通常比 SSR(服务器端渲染)更快。因此,我决定为此使用 AJAX 而不是 Go。如果你使用 Go 模板的 {{range}} 来创建列表,页面将在最后一条记录加载完成后才会完全渲染。以下是我使用 AJAX(CSR)处理 3000 条记录的尝试:https://form.go4webdev.org/aggrid。这里是使用 Go(SSR)的相同示例:https://goapi.go4webdev.org/ques。速度上没有太大差异,但 SSR 渲染某些部分会有延迟。

更多关于Golang模板渲染方案及替代方案讨论的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


在多重循环中使用 html/template 时,性能瓶颈通常出现在每个循环迭代中的转义操作。一次性转义整个数据集可以显著提升性能。以下是几种解决方案:

1. 预转义数据方案

在渲染前对数据进行预转义,避免模板引擎在循环中重复转义:

import (
    "html/template"
    "strings"
)

type SafeThread struct {
    Title template.HTML  // 使用 template.HTML 类型避免重复转义
}

type ListThreads struct {
    Threads []*SafeThread
}

// 预转义函数
func escapeThreads(threads []*Thread) []*SafeThread {
    safeThreads := make([]*SafeThread, len(threads))
    for i, t := range threads {
        safeThreads[i] = &SafeThread{
            Title: template.HTML(template.HTMLEscapeString(t.Title)),
        }
    }
    return safeThreads
}

// 使用示例
func renderThreads(w http.ResponseWriter, threads []*Thread) {
    safeThreads := escapeThreads(threads)
    p := ListThreads{Threads: safeThreads}
    tmpl.Execute(w, &p)
}

模板保持不变:

{{ range .Threads }}
{{ .Title }}
{{ end }}

2. 使用 template.HTML 构建字符串

对于更复杂的场景,可以构建完整的 HTML 字符串:

import (
    "bytes"
    "html/template"
    "strings"
)

func renderThreadsOptimized(w http.ResponseWriter, threads []*Thread) {
    var buf bytes.Buffer
    
    // 一次性构建所有线程的 HTML
    for _, t := range threads {
        buf.WriteString("<div>")
        buf.WriteString(template.HTMLEscapeString(t.Title))
        buf.WriteString("</div>\n")
    }
    
    // 将构建好的 HTML 作为安全内容传递
    data := struct {
        Content template.HTML
    }{
        Content: template.HTML(buf.String()),
    }
    
    tmpl := template.Must(template.New("index").Parse(`
        <div class="threads-container">
            {{.Content}}
        </div>
    `))
    
    tmpl.Execute(w, data)
}

3. Jet Template 性能对比

Jet Template 确实在性能方面有优势,特别是在复杂循环场景中:

import (
    "github.com/CloudyKit/jet/v6"
    "net/http"
)

var jetViews = jet.NewSet(
    jet.NewOSFileSystemLoader("./templates"),
    jet.InDevelopmentMode(),
)

func renderWithJet(w http.ResponseWriter, threads []*Thread) {
    t, err := jetViews.GetTemplate("index.jet")
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    
    vars := make(jet.VarMap)
    vars.Set("threads", threads)
    
    if err := t.Execute(w, vars, nil); err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
    }
}

Jet 模板文件 index.jet

{{ range $i, $thread := threads }}
<div class="thread-{{ $i }}">
    {{ $thread.Title }}
</div>
{{ end }}

4. 基准测试示例

展示性能差异的基准测试:

import (
    "html/template"
    "testing"
    "bytes"
)

func BenchmarkHTMLTemplate(b *testing.B) {
    tmpl := template.Must(template.New("test").Parse(`
        {{ range .Threads }}
        <div>{{ .Title }}</div>
        {{ end }}
    `))
    
    threads := make([]*Thread, 1000)
    for i := range threads {
        threads[i] = &Thread{Title: "Test Thread " + string(rune(i))}
    }
    
    data := ListThreads{Threads: threads}
    var buf bytes.Buffer
    
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        tmpl.Execute(&buf, data)
        buf.Reset()
    }
}

func BenchmarkPreEscaped(b *testing.B) {
    tmpl := template.Must(template.New("test").Parse(`
        {{ range .Threads }}
        <div>{{ .Title }}</div>
        {{ end }}
    `))
    
    threads := make([]*Thread, 1000)
    for i := range threads {
        threads[i] = &Thread{Title: "Test Thread " + string(rune(i))}
    }
    
    safeThreads := escapeThreads(threads)
    data := ListThreads{Threads: safeThreads}
    var buf bytes.Buffer
    
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        tmpl.Execute(&buf, data)
        buf.Reset()
    }
}

5. 混合方案

对于需要部分转义和部分安全内容的场景:

type MixedThread struct {
    Title       template.HTML
    Description string  // 这个字段会在模板中自动转义
    UserInput   string  // 用户输入,需要转义
}

func prepareMixedData(threads []*Thread) []*MixedThread {
    mixed := make([]*MixedThread, len(threads))
    for i, t := range threads {
        mixed[i] = &MixedThread{
            Title:       template.HTML(t.Title), // 假设 Title 已安全
            Description: t.Description,
            UserInput:   t.UserInput,
        }
    }
    return mixed
}

模板:

{{ range .Threads }}
<div class="thread">
    <h2>{{ .Title }}</h2>
    <p>{{ .Description }}</p>
    <div class="user-content">{{ .UserInput }}</div>
</div>
{{ end }}

Jet Template 在大型循环和复杂模板中确实能提供更好的性能,特别是在不需要自动转义的场景中。预转义方案在保持 html/template 安全性的同时提供了性能提升。选择方案应根据具体需求:需要最高性能时考虑 Jet,需要平衡安全性和性能时使用预转义方案。

回到顶部