返回

Template Introduction

Go标准库之模版渲染介绍。


模板渲染是指根据特定的模板语法,将这些模板渲染成文本,通常用于生成 HTML邮件配置文件等。在Go语言中提供了两个标准库来渲染模板,它们都具有相同的模板语法,分别是text/templatehtml/template ,接下来将介绍这两个标准库如何使用。


快速使用

二者都是go语言的标准库,直接使用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package main

import (
	"log"
	"os"

	"text/template"
	//"html/template"
)

func main() {
	const tmpl = "Hello, {{.Name}}! Your message is: {{.Message}}"

	data := struct {
		Name    string
		Message string
	}{
		Name:    "World",
		Message: "<script>alert('XSS');</script>",
	}

	t := template.New("example")
	t, err := t.Parse(tmpl)
	if err != nil {
		log.Fatalf("Parsing template: %s", err)
	}

	err = t.Execute(os.Stdout, data)
	if err != nil {
		log.Fatalf("Excuting templete: %s", err)
	}
}

使用template库很简单,只需要创建Template对象(模板不是文件需要指定模板的名称),调Parse方法解析模板,最后调Execute方法指定输出的地方和数据,就能渲染模板。

模板是一串字符串,由模板语法组成。模板语法都包含在{{}}中间,其中{{.}}中的点表示当前对象。输出的地方是一个io.Writer。数据常用的是结构体对象或map类型。渲染模板是根据模板语法将数据绑定到模板中

上面程序运行输出:

1
2
$ go run main.go                                 
Hello, World! Your message is: <script>alert('XSS');</script>

注释掉"text/template"换成"html/template"输出:

1
2
$ go run main.go 
Hello, World! Your message is: &lt;script&gt;alert(&#39;XSS&#39;);&lt;/script&gt;

二者的主要区别就是html/template会将渲染的HTML内容转义。意思就是让绑定的HTML数据变成普通文本,失去其原来的作用,能够防止跨站脚本攻击 (XSS)。因此当我们的模板是HTML时,推荐使用html/template包渲染,并且该包也是专门为渲染HTML模板准备的,确保生成的 HTML 是安全的。


template介绍

在 Go 语言中,text/templatehtml/template 都是用于模板处理的包,它们在功能上有许多相似之处,但也有重要的区别,尤其是在处理 HTML 时。

作用

  • text/template:适用于生成纯文本内容,比如邮件、日志、配置文件等。这些内容不涉及 HTML 安全性问题。
  • html/template:专门用于生成 HTML 内容,提供了防止 XSS 攻击的保护机制。

共同点:

  • 模板解析:两个包都使用类似的语法来解析模板,支持条件语句、循环、自定义函数等。
  • 数据绑定:都可以将结构体、映射等数据绑定到模板中,从而生成动态内容。
  • 模板执行:都使用 ExecuteExecuteTemplate 方法来执行模板,将结果输出到 io.Writer

不同点

  • 主要区别:

    • 在模板中插入用户提供的数据时,html/template 会自动对这些数据进行 HTML 转义,确保生成的 HTML 是安全的,能够防止跨站脚本攻击 (XSS)。而text/template包不会。
  • 内置的模板函数同名,但功能可能不一样。

跨站脚本攻击(XSS):大概意思是脚本不是自己网站提供的,是别人恶意放到你网站上的。

当用户访问你的网站时,恶意脚本就会运行。

恶意脚本可能做的事:

  1. 窃取 Cookie 和会话信息: 攻击者可以通过恶意脚本窃取用户的 Cookie 和会话信息,从而冒充用户进行操作。
  2. 劫持用户会话: 攻击者可以利用窃取的会话信息,劫持用户的会话,进行恶意操作。
  3. 伪造请求: 恶意脚本可以在用户不知情的情况下发起伪造请求,执行一些用户未授权的操作。
  4. 传播蠕虫: 恶意脚本可以通过 XSS 漏洞传播蠕虫,自动感染访问受害页面的用户。
  5. 虚假内容: 恶意脚本可以修改页面内容,显示虚假信息欺骗用户。

防御XSS攻击

  1. 输出编码

    • 在将用户输入的数据输出到 HTML 内容时,进行 HTML 转义,防止恶意脚本注入。

      • html/template包就会自动转义HTML内容。
    • 在属性中输出数据时,对数据进行属性转义。

  2. 输入验证

    • 对用户输入的数据进行严格验证和过滤,只允许符合预期格式的数据通过。
  3. 使用安全的 JavaScript

    • 避免直接使用 innerHTML,改用 textContent 或其他安全的 DOM 操作方法。
  4. 内容安全策略 (Content Security Policy, CSP)

    • 配置 CSP 头,通过白名单机制限制允许执行的脚本来源,减少 XSS 攻击的风险。

      1
      
      Content-Security-Policy: script-src 'self'
      

template使用

提前介绍模板语法中的定义模板

  • 模板就是一串字符串或一个文本文件。

  • 当为一串字符串时,可能是主模板,也可能是子模板。

    • 没有定义子模板时,是主模板。反之就是子模板。
  • 当为一个文本文件时,相对文件,文件名就是主模板。

    • 通常会将目录/文件名设为该文件的子模板,以表示主模板。防止不同目录出现同名文件。此时渲染整个文件,就需要指定子模板名。
  • 定义子模板:用define内置模板函数,后面更子模板名,是字符串类型,用双引号括起来。结束用end函数标识。中间就是子模板的内容。如:

    templates/default/index.tmpl

    1
    2
    3
    
    {{ define "default/index.tmpl" }}
    Hello, {{ .Name }}! Your message is: {{ .Message }}
    {{ end }}
    

    此时渲染这个文件,就需要用指定模板名的方法。

更多内容参考

渲染Template对象

渲染Template对象就是渲染模板,用到的方法是func (t *Template) Execute(wr io.Writer, data any) errorfunc (t *Template) ExecuteTemplate(wr io.Writer, name string, data any) error

二者的区别是:

  • Execute方法用于渲染主模板

    • 只有一个模板,这个模板就是主模板。
    • 有多个模板时,定义或解析模板时最先创建的那个模板,或是执行时作为入口点的模板没有指定模板名的都会先渲染),最先渲染的模板就是主模板
    • ExecuteTemplate方法用于渲染指定模板名的模板。
  • 总结:

    • 只有一个模板用Execute方法,也可以用ExecuteTemplate方法,当需要指定模板名。
    • 有多个模板用ExecuteTemplate方法,需要指定模板名。如果此时用Execute方法将只渲染主模板
    • func (t *Template) DefinedTemplates() string方法可以查看定义的所以模板。返回的字符串格式为; defined templates are:...

创建Template对象

创建Template对象的方法主要有以下几种:

解析并创建Template对象

  1. template.ParseFiles: 从文件中解析模板并创建Template对象,可以一次解析一个或多个文件。

    1
    2
    3
    4
    
    t, err := template.ParseFiles("templates/file1.tmpl", "templates/file2.tmpl")
    if err != nil {
        // handle error
    }
    
  2. template.ParseGlob: 使用通配符模式解析一组模板文件并创建Template对象。(常用

    1. 所有模板都放在templates目录下(没有分类保存):

      1
      2
      3
      4
      
      t, err := template.ParseGlob("templates/*.tmpl")
      if err != nil {
          // handle error
      }
      
    2. 所有模板都放在templates目录下并分类保存

      需要多匹配一级分类目录,目录用/**/表示该级目录(只能匹配一级)。两级用/**/**,以此类推。如:

      1
      2
      3
      4
      
      t, err := template.ParseGlob("templates/**/*.tmpl")
      if err != nil {
          // handle error
      }
      

      将匹配templates目录下的所有一级目录下的所有*.tmpl文件。

      分目录保存,为防止不同目录出现同文件名,造成模板丢失,通常会将目录/文件名设为该文件的子模板,以表示主模板。防止不同目录出现同名文件此时渲染整个文件,就需要指定子模板名。

创建空的命名模板对象

  1. template.New: 创建一个命名模板的基础模板对象,不会解析任何模板内容。
    • 创建命名模板对象之后,还需要解析模板才能使用。
    • 通常与Parse常用)、ParseFilesParseGlob等方法一起使用。使用方法与同名函数一直。只要是Template对象都能使用这几个方法。
    • 区别是
      • New方法创建空的命名模板后(这模板就是主模板),在用Parse等方法解析模板:
        • 如果解析的模板没有命名且非空,则会覆盖主模板的内容。
        • 如果解析的模板有命名,则会为主模板添加子模板。
        • 以上情况为字符串模板。
        • 解析文件都是为主模板添加子模板。因为文件名默认为一个模板名。

template.Must: 是一个帮助函数,用于包装解析函数(如ParseParseFilesParseGlob等)的返回值。如果解析过程中发生错误,它会导致程序在运行时崩溃。常用于简化创建模板对象代码

示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package main

import (
    "os"
    "text/template"
)

func main() {
    // 创建一个新的模板并解析内容
    tmpl := template.New("example")
    tmpl, err := tmpl.Parse("Hello, {{.Name}}!")
    if err != nil {
        panic(err)
    }

    // 直接解析模板字符串,使用帮助函数简化创建模板对象
    tmpl = template.Must(template.New("example").Parse("Hello, {{.Name}}!"))

  
    // 从多个文件解析模板
    tmpl, err = template.ParseFiles("example1.tmpl", "example2.tmpl")
    if err != nil {
        panic(err)
    }

    // 使用通配符从文件夹解析模板
    tmpl, err = template.ParseGlob("templates/*.tmpl")
    if err != nil {
        panic(err)
    }
    
    // 简化创建模板对象,解析失败会自动抛出panic
     tmpl = template.Must(template.ParseGlob("templates/**/*.tmpl"))

    // 执行模板
    err = tmpl.Execute(os.Stdout, map[string]string{"Name": "World"})
    if err != nil {
        panic(err)
    }
}

模板语法介绍

基础语法

  1. 模板语法都包含在{{}}中间。{{.}}:点表示当前对象。

    • 数据是结构体对象时,直接用.属性名访问属性。

    • 数据是映射时,直接用.键名访问值。

    • 键名或属性名要一一对应。

    • 以上两个可以互相嵌套访问。示例:

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      
      package main
      
      import (
      	"log"
      	"os"
      
      	"text/template"
      	//"html/template"
      )
      
      type User struct {
      	Name  string
      	Age   int
      	Other map[string]any
      }
      
      func main() {
      	t := template.Must(template.New("example").Parse(`{{ .title }}
      姓名:{{ .user.Name }}
      年龄:{{ .user.Age }}
      QQ: {{ .user.Other.QQ }}
      学校:{{ .user.Other.school }}`))
      
      	err := t.Execute(os.Stdout, map[string]any{
      		"title": "用户信息",
      		"user": User{
      			Name: "Jack",
      			Age:  18,
      			Other: map[string]any{
      				"QQ":     9527,
      				"school": "野鸡大学",
      			},
      		},
      	})
      	if err != nil {
      		log.Fatalf("Excuting templete: %s", err)
      	}
      }
      

      运行输出:

      1
      2
      3
      4
      5
      6
      
      $ go run main.go
      用户信息
      姓名:Jack
      年龄:18
      QQ: 9527
      学校:野鸡大学
      
    • 数据是基本数据类型时,如int、bool、string。直接用.表示值。

    • 数据是切片、数组、映射等集合类型时,需要遍历对象。(稍后介绍)

      • 注意映射有两种方法访问值:.键名或者循环遍历
  2. 格式: {{ 模板表达式 }}。模板表达式与括号之间建议用空格隔开。如果有空格渲染时会自动移除。

    1. 模板表达式可以是.、函数、变量等组成。

    2. 表示式如果没有值,渲染后会用<no value>表示。

    3. 在两个括号内添加-与括号之间不能有空格,与表达式之间必须有空格)表示删除空白

      • 删除的是渲染结果与周围字符之间的空白。
      • -在那边就表示删除那边与字符之间的空白。都有表示左右两边字符都删除。
      • 空白指空格、换行。
      1
      2
      
      "{{ 23 -}} < {{ 45 }}" // 输出23< 45
      "{{ 23 -}} < {{- 45 }}" // 输出23<45
      
  3. 注释格式: {{/* 注释内容 */}}注意/与括号之间不能有空格。执行时会忽略。可以多行。注释不能嵌套。*与注释内容可以没有空格。

  4. 定义模板前面已经介绍了。

    • 补充:定义子模板语句如果独占一行,虽然渲染后为空,但是会独占一空行,注释也会,后面的定义变量也是。
    • 为不影响渲染之后的结构,可以将他们与内容写在一行。这样即使注释换行也不会占一空行。模板语法与其他内容之间也不要有空格,否则渲染之后会保留空格。
    • 后来才知道删除空格的作用最优解决方案:使用用-可以删除空格、换行能达到美观而不影响渲染结果
  5. 模板变量

    • 在模版中可以自定义变量, 类似golang使用:=符号定义变量,用来保存传入模板的数据或其他语句生成的结果。语法为:{{ $变量名 := 数据 }}

    • 为存在的变量赋值{{ $变量名 = 数据 }}

    • 引用变量{{ $变量名 }}

    • 数据可以是字符串(用双引号括起来)、整型、表达式的值。

    • 引用变量可以与表达式组合使用。

    • 上面示例可修改为:

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      
      package main
      
      import (
      	"log"
      	"os"
      
      	//"text/template"
      	"html/template"
      )
      
      type User struct {
      	Name  string
      	Age   int
      	Other map[string]any
      }
      
      func main() {
      	t := template.Must(template.New("example").
      		Parse(`{{ define "aaa" }}
      {{ .title }} {{ $other := .user.Other }}
      姓名:{{ .user.Name }}{{/* 这是注释
      换行了 */}}
      年龄:{{ .user.Age }}
      QQ: {{ $other.QQ }}
      学校:{{ $other.school }}{{ end }}`))
      
      	err := t.ExecuteTemplate(os.Stdout, "aaa", map[string]any{
      		"title": "用户信息",
      		"user": User{
      			Name: "Jack",
      			Age:  18,
      			Other: map[string]any{
      				"QQ":     9527,
      				"school": "野鸡大学",
      			},
      		},
      	})
      	if err != nil {
      		log.Fatalf("Excuting templete: %s", err)
      	}
      }
      

      运行输出:

      1
      2
      3
      4
      5
      6
      7
      
      $ go run main.go
      
      用户信息 
      姓名:Jack
      年龄:18
      QQ: 9527
      学校:野鸡大学
      

      可以对比一下有什么区别。(定义子模块的头部独占一空行,解决办法:与title同行且在前面)

      可以不用修改结构,使用-删除空白推荐使用

流程控制语句

介绍流程控制语句前,先介绍比较函数(也叫逻辑运算函数)。将逻辑运算封装成了函数形式

常用的比较函数如下:

  • eq:等于
  • ne:不等于
  • lt:小于
  • le:小于等于
  • gt:大于
  • ge:大于调用

比较函数后面跟两个可比较的参数,用空格分隔开。返回true或者false,对应类型的零值为false,其余为true。如:

1
2
{{ lt 22 33 }} // 输出true
{{ ge 22 33 }} // 输出false

比较函数可以与条件判断(if语句)组合使用。

模版语法的流程控制语句主要指if/range/with三种语句。

条件判断

ifelse, else if)语句用于根据条件来控制模板的输出。可以使用elseelse if来处理其他情况。

格式

1
2
3
4
5
6
7
{{if condition}}
    <!-- 当condition为true时输出的内容 -->
{{else if otherCondition}}
    <!-- 当otherCondition为true时输出的内容 -->
{{else}}
    <!-- 当condition和otherCondition都为false时输出的内容 -->
{{end}}

if语句后面跟end表示结束。

示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
package main

import (
	"bufio"
	"errors"
	"fmt"
	"log"
	"os"
	"strings"
	"text/template"

	"github.com/spf13/cast"
)

var ErrExit = errors.New("exit")

const tmpl = `你的成绩:{{ if lt . 60 }}不及格{{ else if eq . 60 }}刚好及格{{ else }}及格了{{ end }}`

func ReadScore() (int, error) {
	fmt.Print("请输入你的成绩(q:退出):")
	r := bufio.NewReader(os.Stdin)
	s, err := r.ReadString('\n')
	if err != nil {
		return 0, fmt.Errorf("fail to read string: %w", err)
	}

	if strings.Contains(strings.ToLower(s), "q") {
		return 0, ErrExit
	}

	score, err := cast.ToIntE(strings.TrimSpace(s))
	if err != nil {
		return 0, fmt.Errorf("conversion to integer failed %w", err)
	}

	return score, nil
}

func DisplayGrade(data int, t *template.Template) error {
	err := t.Execute(os.Stdout, data)
	if err != nil {
		return fmt.Errorf("fail to execute template: %w", err)
	}
	fmt.Println()
	return nil
}

func main() {
	t := template.Must(template.New("example").Parse(tmpl))

	for {
		score, err := ReadScore()
		if err != nil {
			if errors.Is(err, ErrExit) {
				log.Println("Exit successful")
				return
			}
			log.Printf("Fail to read score: %s", err)
			continue
		}

		err = DisplayGrade(score, t)
		if err != nil {
			log.Printf("Fail to diaplay grade: %s", err)
		}
	}
}

运行输出:

1
2
3
4
5
6
7
8
9
$ go run main.go 
请输入你的成绩(q:退出):55
你的成绩:不及格
请输入你的成绩(q:退出):60
你的成绩:刚好及格
请输入你的成绩(q:退出):60
你的成绩:刚好及格
请输入你的成绩(q:退出):q
2024/06/04 15:44:17 Exit successful

循环

循环range语句用于迭代数组、切片、映射等集合类型的数据。跟go语言的for-range差不多,甚至比go语言更简洁。

格式

1
2
3
{{range 集合类型数据}}
    {{.}}
{{end}}

遍历集合类型数据之后,可以用.访问每一个集合元素的值

注意range范围内,.的作用域仅在range范围内,无法访问外部对象

如果需要访问索引,可以使用两个变量接收:

1
2
3
4
5
{{range $index, $element := 集合类型数据}}
    Index: {{$index}}, Value: {{$element}}
{{else}} // 看情况是否使用
没有数据可遍历
{{end}}

range语句后面跟end表示结束。它们之间还可以嵌入else语句,当集合类型数据长度为0时将执行else语句。

1
2
3
4
5
{{range 集合类型数据}}
    {{.}}
{{else}} // 看情况是否使用
没有数据可遍历
{{end}}

if语句组合使用:

1
2
3
4
5
6
7
{{range .Items}}
    {{if .IsActive}}
        Active item: {{.Name}}
    {{end}}
{{else}}
    No items found.
{{end}}

示例1:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
{{define "userList"}}
    <ul>
    {{range .Users}}
        <li>
        {{if .Active}}
            Active user: {{.Name}}
        {{else}}
            Inactive user: {{.Name}}
        {{end}}
        </li>
    {{else}}
        <li>No users found.</li>
    {{end}}
    </ul>
{{end}}

示例2:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package main

import (
	"fmt"
	"html/template"
	"log"
	"os"
)

func main() {
	const tmpl = `爱好:{{ range . }}
{{ . }}{{ else }}没有任何爱好{{ end }}`
	t := template.Must(template.New("example").Parse(tmpl))

	data := []string{"看电影", "跑步", "打篮球", "看小说"}
	err := t.Execute(os.Stdout, data)

	assertExecErr := func(err error) {
		if err != nil {
			log.Fatalf("Fail to execute template: %s", err)
		}
	}
	assertExecErr(err)
	fmt.Println("\n----------------------")
	err = t.Execute(os.Stdout, []string{})
	assertExecErr(err)

	fmt.Println("\n----------------------")
	const tmpl2 = `爱好2:{{ range $index, $value := . }}
Index: {{ $index }}, Value: {{ $value }}{{ else }}没有任何爱好{{ end }}`

	t2 := template.Must(template.New("example2").Parse(tmpl2))

	err = t2.Execute(os.Stdout, data)
	assertExecErr(err)

	fmt.Println("\n----------------------")
	err = t2.Execute(os.Stdout, []string{})
	assertExecErr(err)
}

运行输出:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
$ go run main.go 
爱好:
看电影
跑步
打篮球
看小说
----------------------
爱好:没有任何爱好
----------------------
爱好2:
Index: 0, Value: 看电影
Index: 1, Value: 跑步
Index: 2, Value: 打篮球
Index: 3, Value: 看小说
----------------------
爱好2:没有任何爱好

变量声明(with, define, block

with

with语句用于重定义模板的作用域。常用于缩短长的字段访问路径。就是可以缩短结构体对象属性的访问路径。简单理解:重定义.的作用域(var . := .User):将当前对象的User属性复制给.。如:

1
2
3
4
5
6
7
8
9
// 访问Data结构体下的user属性
Name: {{.User.Name}}
Email: {{.User.Email}}

// 使用with
{{with .User}}
    Name: {{.Name}}
    Email: {{.Email}}
{{end}}

注意:range和with语句都改变了点(.)引用的数据,那么如果想要在range和with语句中引用模版参数,请先将(点(.)赋值给一个自定义变量, 然后在range和with中通过自定义变量,引用模版参数。

示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main

import (
	"html/template"
	"log"
	"os"
)

type User struct {
	Name  string
	Email string
}

type Data struct {
	User
}

func main() {
	const tmpl = `用户信息:{{ with .User }}
姓名: {{ .Name }}
邮箱:{{ .Email }}
{{ end }}`
	t := template.Must(template.New("example").Parse(tmpl))

	data := Data{User{Name: "arlettebrook", Email: "arlettebrook@proton.me"}}

	err := t.Execute(os.Stdout, data)
	if err != nil {
		log.Fatalf("Fail to execute template: %s", err)
	}
}

运行输出:

1
2
3
4
$ go run main.go 
用户信息:
姓名: arlettebrook
邮箱:arlettebrook@proton.me
define

define语句用于定义一个模板,通常用于复用模板片段,实现模板嵌套

1
2
3
{{define "templateName"}}
    <!-- 模板内容 -->
{{end}}

可以在其他地方使用template来引用:

1
{{template "templateName" .}}

template函数的第一个参数是模板名字,第二个参数是当前模板参数, 在子模板内部也是通过点( . ),引用模板参数。

当子模板没有参数时,.是可选的。

注意

  • 引入子模板带参数的时候别忘记最后的.用于传递模板参数
  • 定义子模版不能嵌套。

示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package main

import (
	"html/template"
	"log"
	"os"
)

type User struct {
	Name  string
	Email string
}

type Data struct {
	User
}

func main() {
	const info = `{{ define "userinfo.tmpl" }}{{ with .User }}
姓名: {{ .Name }}
邮箱:{{ .Email }}{{ end }}{{end}}`
	const tmpl = `用户信息:{{template "userinfo.tmpl" . }}`

	t := template.Must(template.New("example").Parse(tmpl))
	t, err := t.Parse(info)
	if err != nil {
		log.Fatalf("Fail to parse template: %s", err)
	}

	data := Data{User{Name: "arlettebrook", Email: "arlettebrook@proton.me"}}

	err = t.Execute(os.Stdout, data)
	if err != nil {
		log.Fatalf("Fail to execute template: %s", err)
	}
}

运行输出:

1
2
3
4
$ go run main.go
用户信息
姓名: arlettebrook
邮箱arlettebrook@proton.me

模板管理

上面的例子,我们将模板代码定义在一个变量或者常量中,这个只是用于演示,实际项目中模板代码通常非常多,建议大家按如下方式组织模板代码:

  • 一个模板的模板代码,保存在一个模板文件中,模板文件名后缀为tpl或tmpl或者其他,如html。编码方式是utf-8。
  • 所有的模板代码都定义在子模板中,方便根据模板名字进行渲染。
  • 所以模板都建议放在templates目录下。使用ParseGlob匹配模式批量解析模板并创建模板对象。
  • 可以将公共的子模板定义在一个文件中common.tpl

示例:

模板目录templates, 下面分别按功能模块创建不同的模板文件。

创建公共模板文件: templates/common.tpl。 主要用于保存一些公共的模板定义:

1
2
3
4
5
6
7
{{define "common1"}}
这里是共享模块1
{{end}}

{{define "common2"}}
这里是共享模块2
{{end}}

创建mod1模块的模板文件: templates/mod1.tpl:

1
2
3
4
{{define "mod1"}}
这里是模块1
{{- template "common1"}}
{{end}}

创建mod2模块的模板文件: templates/mod2.tpl:

1
2
3
4
{{define "mod2"}}
这里是模块2
{{- template "common2"}}
{{end}}

渲染模板代码:

1
2
3
4
5
6
7
8
//创建template对象,并且加载templates目录下面所有的tpl模板文件。
t := template.Must(template.ParseGlob("templates/*.tpl"))

// 渲染mod1子模板
t.ExecuteTemplate(os.Stdout, "mod1", nil)

// 渲染mod2子模板
t.ExecuteTemplate(os.Stdout, "mod2", nil)

运行输出:

1
2
3
4
5
6
7
8

这里是模块1
这里是共享模块1



这里是模块2
这里是共享模块2
block

block语句用于定义一个可重写的模板块。通常用于嵌套模板或布局模板。意思就是:没有重写的模板将使用默认模板渲染。

格式:

1
2
3
{{define "base"}}
    Base template: {{block "content" .}}Default content{{end}}
{{end}}

子模板可以重写block

1
2
3
4
5
6
7
{{define "content"}}
    Custom content
{{end}}

{{define "home"}}
{{template "base" .}}
{{end}}

注意事项:

  1. 定义可重写的模板块,用block语句,后面跟两个参数:重写的模块名和传递的模板参数(.)。二中缺一不可
    • block语句通常与在define语句组合使用,用于在定义模板中定义可重写的模板
  2. 重写模板用define语句。语法格式与定义模板一样。
    • 需要注意的是不能嵌套define语句。
  3. 重写模板与定义重写模板不能在同一个模板文件中
  4. 确保define语句在模板文件的顶层定义。
  5. block语句仅在go版本1.19或更高版本以上支持text/templatehtml/template,以外版本仅支持html/template包或都不支持。

示例:

templates/default/base.tmpl:

1
2
3
4
{{ define "base" -}}
default/index.tmpl: Hello, {{.Name}}! Your message is: {{.Message}}
{{ block "custom" . }}custom template {{ .Name }} {{ end }}
{{- end }}

templates/default/index.tmpl:

1
2
3
4
5
{{ define "custom" }} ===custom template=== {{ .Message }}  {{ end }}

{{ define "default/index.tmpl" -}}
{{ template "base" .}}
{{- end }}

main.go:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import (
	"log"
	"os"
	"text/template"
)

type User struct {
	Name    string
	Message string
}

func main() {
	t, err := template.ParseGlob("templates/**/*.tmpl")
	if err != nil {
		log.Fatalf("Fail to parse template: %s", err)
	}

	data := User{Name: "Jack", Message: "你好"}
	err = t.ExecuteTemplate(os.Stdout, "default/index.tmpl", data)
	if err != nil {
		log.Fatalf("Fail to execute template: %s", err)
	}
}

运行输出:

1
2
3
$ go run main.go 
default/index.tmpl: Hello, Jack! Your message is: 你好
 ===custom template=== 你好

实现重写模板。


模板函数介绍

go的模板引擎为我们提供了函数机制,方面我们在处理模板时执行一些特定的功能,例如格式化输出内容、字母大小写转换等等。

模板函数调用语法

语法格式:

1
functionName [Argument...]

Argument参数是可选的,如果有多个参数,参数直接用空格分隔。

注意:模板语法都是在{{}}中的,函数调用也是。

示例:

1
{{ html "<script>alert('XSS');</script>" }}

html预定义函数,将html内容进行转义,防止XSS攻击。

渲染将输出:

1
&lt;script&gt;alert(&#39;XSS&#39;);&lt;/script&gt;

多个函数参数的示例:

1
{{ printf "%s: %d" "年龄" 18 }}

printf函数主要用于格式化输出字符串,是fmt.Sprintf函数的别名,用法跟fmt.Sprintf函数一样,区别就是模板函数的参数用空格隔开。

这里为printf函数传递了3个参数。

渲染将输出:

1
年龄: 18

预定义模板函数

预定义模板函数也可以叫内置模板函数,是模板引擎预定义好了的,可以直接在模板中拿来使用。下面介绍常用的内置函数:

  1. 前面介绍的比较函数(关系运算函数)也是属于预定义函数。

  2. 也将逻辑运算封装成了函数形式:

    1
    2
    3
    4
    5
    6
    
    and 表达式1 表达式2
    	表达式1和表达式2都为真的时候返回true
    or 表达式1 表达式2
        表达式1和表达式2其中一个为真的时候返回true
    not 表达式
        表达式为false则返回true, 反之返回false
    

    官网解释看不明白,差不多就是上面的意思。

    提示: 关系运算和逻辑运算函数通常跟if语句一起使用。

    示例:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    
    {{$x := 100}}
    
    //等价于$x == 100
    {{if eq $x 100}}
    ...代码...
    {{end}}
    
    //等价于$x < 100
    {{if lt $x 500}}
    ...代码...
    {{end}}
    
    //等价于$x >= 100
    {{if ge $x 500}}
    ...代码...
    {{end}}
    
    //等价于$x > 50 && $x < 200
    //这里调用了and函数和gt、lt三个函数, gt和lt函数的结果作为and的参数,gt和lt函数调用分别用括号包括起来
    {{if and (gt $x 50) (lt $x 200)}}
    ...代码...
    {{end}}
    
    {{$y := 200}}
    
    //等价于$x > 100 || $y > 100
    {{if or (gt $x 100) (gt $y 100)}}
    ...代码...
    {{end}}
    
  3. 更多内置函数:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    
    len
        返回它的参数的整数类型长度,可以计算数组长度。
        	数组大小: {{len .}}
        	//模板参数定义如下:
    		a := []int{1,2,3,4}
    		渲染输出:
    		数组大小: 4
    index
        返回其第一个参数(通常为数组、切片或映射)的第 N 个元素,N 由后续参数指定。
        如"index x 1 2 3"返回x[1][2][3]的值;每个被索引的主体必须是数组、切片或者字典。
        	data := []string{"first", "second", "third"}
    		tmpl := `Second element: {{ index . 1 }}`
    		渲染输出:Second element: second
    print
        即fmt.Sprint
    printf
        即fmt.Sprintf
    println
        即fmt.Sprintln
        主要用于格式化字符串,是go fmt.Sprintf函数的别名,前面的例子已经介绍。
    html
        返回与其参数的文本表示形式等效的转义HTML。
        将其参数作为安全的 HTML 输出。
        这个函数在html/template中不可用。
    urlquery
        以适合嵌入到网址查询中的形式返回其参数的文本表示的转义值。
        将其参数编码为 URL 查询参数。
        这个函数在html/template中不可用。
        主要用于url编码。
        	/search?keyword={{urlquery "搜索关键词"}}
        	/search?keyword=%E6%90%9C%E7%B4%A2%E5%85%B3%E9%94%AE%E8%AF%8D
    js
        返回与其参数的文本表示形式等效的转义JavaScript。
        将其参数作为安全的 JavaScript 输出。
        	{{ js "<script>alert('Hello, World!');</script>" }}
        	渲染输出:
        	\u003Cscript\u003Ealert(\'Hello, World!\');\u003C/script\u003E
    call
    	调用其第一个参数指定的函数,其余参数作为函数参数传递。
        执行结果是调用第一个参数的返回值,该参数必须是函数类型,其余参数作为调用该函数的参数;
        如"call .X.Y 1 2"等价于go语言里的dot.X.Y(1, 2);
        其中Y是函数类型的字段或者字典的值,或者其他类似情况;
        call的第一个参数的执行结果必须是函数类型的值(和预定义函数如print明显不同);
        该函数类型值必须有1到2个返回值,如果有2个则后一个必须是error接口类型;
        如果有2个返回值的方法返回的error非nil,模板执行会中断并返回给调用模板执行者该错误;
    

    call示例(可以先看自定义模板函数部分):

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
    package main
    
    import (
        "fmt"
        "os"
        "text/template"
    )
    
    func sayHello(name string) string {
        return fmt.Sprintf("Hello, %s!", name)
    }
    
    func main() {
        tmpl := `{{ call . "Gopher" }}`
        t := template.Must(template.New("example").Funcs(template.FuncMap{
            "sayHello": sayHello,
        }).Parse(tmpl))
        t.Execute(os.Stdout, sayHello)
    }
    

    运行输出:

    1
    
    Hello, Gopher!
    

管道(pipeline)

pipeline 翻译过来可以称为管道或者流水线, pipeline运算的作用是将多个函数调用或者值串起来,从左往右执行,左边执行的结果会传递给右边,形成一个任务流水。

pipeline运算符:| (竖线)

语法格式:

1
command1 | command2 | command3 ... 

command可以是一个值,也可以是一个函数。

示例1:

1
{{ "<script>alert('XSS');</script>" | html }}

这里意思就是将第一个字符串值传递给html函数。

渲染将输出:

1
&lt;script&gt;alert(&#39;XSS&#39;);&lt;/script&gt;

示例2:

1
{{ "关键词" | html | urlquery }}

这个例子就是先将 “关键词” 传递给html函数转义下html标签,然后在将html执行结果传递给urlquery函数进行url编码。

渲染将输出:

1
%E5%85%B3%E9%94%AE%E8%AF%8D

注意:如果函数有多个参数,pipeline运算会将值传递给函数的最后一个参数, 例如: {{ 100 | printf "value=%d" }}, 这里将100传递给printf函数的最后一个参数。

自定义模板函数

内置的模板函数使用有限,我们可以自己定义模板函数。

步骤

  1. 创建自定义函数。

  2. 将自定义函数映射到模板引擎中。用FuncMap函数映射。本质是map类型。可以映射多个。

    • 键值都是自定义的函数名。
  3. 最后调用Funcs方法,将映射添加到模板中。

  4. 注意请在解析前完成以上操作。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
     	// 创建模板并添加自定义函数
        t := template.New("example").Funcs(funcMap)
    
        // 解析匹配指定模式的模板文件
        t = template.Must(t.ParseGlob("templates/*.tmpl"))
    
        or
        // 创建模板并解析
        t := template.Must(template.New("example").Funcs(funcMap).Parse(tmpl))
    

示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package main

import (
	"log"
	"os"
	"strings"
	"text/template"
)

func ToUpper(s string) string {
	return strings.ToUpper(s)
}

func Repeat(word string, count int) string {
	return strings.Repeat(word, count)
}

type Data struct {
	Message string
	Word    string
	Count   int
}

func main() {
	funcMap := template.FuncMap{
		"ToUpper": ToUpper,
		"Repeat":  Repeat,
	}

	const tmpl = `{{ ToUpper .Message }}
{{ Repeat .Word .Count }}`

	data := Data{
		Message: "hello, world!",
		Word:    "Go",
		Count:   3,
	}

	t := template.Must(template.New("example").Funcs(funcMap).Parse(tmpl))

	err := t.Execute(os.Stdout, data)
	if err != nil {
		log.Fatalf("Fail to execute template: %s", err)
	}
}

运行输出:

1
2
3
$ go run main.go
HELLO, WORLD!
GoGoGo

修改默认的标识符

Go标准库的模板引擎使用的花括号{{}}作为标识,而许多前端框架(如VueAngularJS)也使用{{}}作为标识符,所以当我们同时使用Go语言模板引擎和以上前端框架时就会出现冲突,这个时候我们需要修改标识符,修改前端的或者修改Go语言的。这里演示如何修改Go语言模板引擎默认的标识符:

1
2
3
	const tmpl = `{[ printf "==%s==" . ]}`

	t := template.Must(template.New("example").Delims("{[", "]}").Parse(tmpl))

用到的方法是Delims,分别接收两端的分隔符为参数。


应用

在Go语言中,text/templatehtml/template包用于生成文本和HTML输出,常见的应用场景如下:

text/template

  1. 生成配置文件

    通过模板生成动态配置文件,如YAML、JSON等格式,用于不同环境的配置管理。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    
    package main
    
    import (
        "os"
        "text/template"
    )
    
    func main() {
        tmpl, err := template.New("config").Parse(`
        apiVersion: v1
        kind: ConfigMap
        metadata:
          name: {{.Name}}
        data:
          key: {{.Value}}
        `)
        if err != nil {
            panic(err)
        }
    
        data := struct {
            Name  string
            Value string
        }{
            Name:  "example-config",
            Value: "example-value",
        }
    
        err = tmpl.Execute(os.Stdout, data)
        if err != nil {
            panic(err)
        }
    }
    
  2. 生成代码

    自动生成代码文件,如生成CRUD代码、接口实现代码等。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    
    package main
    
    import (
        "os"
        "text/template"
    )
    
    func main() {
        tmpl, err := template.New("crud").Parse(`
        package main
    
        type {{.Name}} struct {
            ID int
            Name string
        }
    
        func ({{.Receiver}} *{{.Name}}) Create() {
            // Create logic
        }
    
        func ({{.Receiver}} *{{.Name}}) Read() {
            // Read logic
        }
    
        func ({{.Receiver}} *{{.Name}}) Update() {
            // Update logic
        }
    
        func ({{.Receiver}} *{{.Name}}) Delete() {
            // Delete logic
        }
        `)
        if err != nil {
            panic(err)
        }
    
        data := struct {
            Name     string
            Receiver string
        }{
            Name:     "User",
            Receiver: "u",
        }
    
        err = tmpl.Execute(os.Stdout, data)
        if err != nil {
            panic(err)
        }
    }
    
  3. 生成文档

    生成报告、邮件、日志等文本文件。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    
    package main
    
    import (
        "os"
        "text/template"
    )
    
    func main() {
        tmpl, err := template.New("report").Parse(`
        Report:
        Name: {{.Name}}
        Date: {{.Date}}
        Summary: {{.Summary}}
        `)
        if err != nil {
            panic(err)
        }
    
        data := struct {
            Name    string
            Date    string
            Summary string
        }{
            Name:    "John Doe",
            Date:    "2024-06-10",
            Summary: "This is a summary of the report.",
        }
    
        err = tmpl.Execute(os.Stdout, data)
        if err != nil {
            panic(err)
        }
    }
    

html/template

html/template包专门用于生成安全的HTML内容,防止XSS(跨站脚本攻击)。以下是一些常见应用:

  1. 生成动态网页

    根据用户输入或数据库内容动态生成HTML页面,适用于Web应用的前端展示。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    
    package main
    
    import (
        "html/template"
        "net/http"
    )
    
    type User struct {
        Name  string
        Email string
    }
    
    func handler(w http.ResponseWriter, r *http.Request) {
        tmpl, err := template.New("user").Parse(`
        <!DOCTYPE html>
        <html>
        <head>
            <title>User Page</title>
        </head>
        <body>
            <h1>Hello, {{.Name}}</h1>
            <p>Email: {{.Email}}</p>
        </body>
        </html>
        `)
        if err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }
    
        user := User{Name: "Alice", Email: "alice@example.com"}
    
        err = tmpl.Execute(w, user)
        if err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
        }
    }
    
    func main() {
        http.HandleFunc("/", handler)
        http.ListenAndServe(":8080", nil)
    }
    
  2. 生成邮件内容

    生成包含HTML格式的邮件内容,用于发送动态邮件。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    
    package main
    
    import (
        "bytes"
        "fmt"
        "html/template"
        "net/smtp"
    )
    
    func main() {
        tmpl, err := template.New("email").Parse(`
        <html>
        <body>
            <h1>Hello, {{.Name}}</h1>
            <p>Thank you for joining our service.</p>
        </body>
        </html>
        `)
        if err != nil {
            panic(err)
        }
    
        var body bytes.Buffer
        data := struct {
            Name string
        }{Name: "Alice"}
    
        err = tmpl.Execute(&body, data)
        if err != nil {
            panic(err)
        }
    
        auth := smtp.PlainAuth("", "your-email@example.com", "your-email-password", "smtp.example.com")
        err = smtp.SendMail("smtp.example.com:587", auth, "your-email@example.com", []string{"recipient@example.com"}, body.Bytes())
        if err != nil {
            panic(err)
        }
    
        fmt.Println("Email sent successfully!")
    }
    
  3. 生成HTML报告

    根据模板生成包含动态内容的HTML报告,适用于生成分析结果、统计数据展示等。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    
    package main
    
    import (
        "html/template"
        "os"
    )
    
    func main() {
        tmpl, err := template.New("report").Parse(`
        <!DOCTYPE html>
        <html>
        <head>
            <title>Report</title>
        </head>
        <body>
            <h1>Report Summary</h1>
            <p>Name: {{.Name}}</p>
            <p>Date: {{.Date}}</p>
            <p>Summary: {{.Summary}}</p>
        </body>
        </html>
        `)
        if err != nil {
            panic(err)
        }
    
        data := struct {
            Name    string
            Date    string
            Summary string
        }{
            Name:    "John Doe",
            Date:    "2024-06-10",
            Summary: "This is a summary of the report.",
        }
    
        err = tmpl.Execute(os.Stdout, data)
        if err != nil {
            panic(err)
        }
    }
    

总结

  • text/template主要用于生成纯文本内容,如配置文件、代码、文档等。
  • html/template主要用于生成安全的HTML内容,如动态网页、邮件内容、HTML报告等。

这两个包都通过模板提供了强大的文本处理功能,可以根据需要选择适用的包来生成所需的输出。


补充

不转义HTML内容

如果你需要安全地显示用户输入的 HTML 内容,可以使用以下几种方法:

  1. 明确信任的内容

    • 当我们使用html/template包渲染时,会自动转义HTML内容,如果不希望转义HTML内容,可以将要渲染的HTML内容定义为template.HTML类型(本质就是字符串),这样就不会自动转义HTML内容。

    • 需要注意的是:对于完全可信的内容,才能使用 template.HTML,以防止XSS攻击。

    • 对应的信任JavaScript类型template.JS :不会自动转义为安全的js代码。

    • 示例:

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      
      package main
      
      import (
      	"log"
      	"os"
      
      	//"text/template"
      	"html/template"
      )
      
      func main() {
      	const tmpl = "Hello, {{.Name}}! Your message is: {{.Message}}"
      
      	data := struct {
      		Name    string
      		Message template.HTML
      	}{
      		Name:    "World",
      		Message: "<script>alert('XSS');</script>",
      	}
      
      	t := template.New("example")
      	t, err := t.Parse(tmpl)
      	if err != nil {
      		log.Fatalf("Parsing template: %s", err)
      	}
      
      	err = t.Execute(os.Stdout, data)
      	if err != nil {
      		log.Fatalf("Excuting templete: %s", err)
      	}
      }
      

      运行输出:

      1
      2
      
      $ go run main.go
      Hello, World! Your message is: <script>alert('XSS');</script>
      
  2. 使用 HTML 白名单

    • 如果用户提供的内容需要部分 HTML 标签,可以使用 HTML 解析库将用户输入的内容过滤,只允许特定的标签和属性通过。这可以使用第三方库如 bluemonday 来实现。

    • 示例:

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      
      package main
      
      import (
      	"html/template"
      	"log"
      	"os"
      
      	"github.com/microcosm-cc/bluemonday"
      )
      
      func sanitizeHTML(input string) template.HTML {
      	p := bluemonday.UGCPolicy() // 使用默认的用户生成内容策略
      	sanitized := p.Sanitize(input)
      	return template.HTML(sanitized)
      }
      
      func main() {
      	const tmpl = `Hello, {{.Name}}! Your message is: {{.Message}}`
      
      	data := struct {
      		Name    string
      		Message template.HTML
      	}{
      		Name: "World",
      		Message: sanitizeHTML("<strong>Bold Text</strong>" +
      			"<script>alert('XSS');</script>"),
      	}
      
      	t, err := template.New("example").Parse(tmpl)
      	if err != nil {
      		log.Fatalf("parsing template: %s", err)
      	}
      
      	err = t.Execute(os.Stdout, data)
      	if err != nil {
      		log.Fatalf("executing template: %s", err)
      	}
      }
      

      运行输出:

      1
      2
      
      $ go run main.go 
      Hello, World! Your message is: <strong>Bold Text</strong>
      
  3. 使用 Markdown

    • 另一种方式是使用 Markdown,将用户输入的 Markdown 转换为安全的 HTML。这种方式适用于允许用户使用简单标记语言格式化内容的场景。用到的第三方库是blackfriday

    • 示例:

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      
      package main
      
      import (
      	"html/template"
      	"log"
      	"os"
      
      	"github.com/russross/blackfriday/v2"
      )
      
      func markdownToHTML(input string) template.HTML {
      	output := blackfriday.Run([]byte(input))
      	return template.HTML(output)
      }
      
      func main() {
      	const tmpl = `Hello, {{.Name}}! Your message is: {{.Message}}`
      
      	data := struct {
      		Name    string
      		Message template.HTML
      	}{
      		Name: "World",
      		Message: markdownToHTML("**Bold Text**\n\n" +
      			"<script>alert('XSS');</script>"),
      	}
      
      	t, err := template.New("example").Parse(tmpl)
      	if err != nil {
      		log.Fatalf("parsing template: %s", err)
      	}
      
      	err = t.Execute(os.Stdout, data)
      	if err != nil {
      		log.Fatalf("executing template: %s", err)
      	}
      }
      

      运行输出:

      1
      2
      3
      4
      
      $ go run main.go 
      Hello, World! Your message is: <p><strong>Bold Text</strong></p>
      
      <p><script>alert(&lsquo;XSS&rsquo;);</script></p>
      

      在这个例子中,任何脚本标签或其他潜在的 XSS 攻击内容都会被转义,确保生成的 HTML 是安全的。

总结

  • 明确信任的内容:对于完全可信的内容,使用 template.HTML
  • 使用 HTML 白名单:使用 HTML 解析库,如 bluemonday,过滤用户输入,只允许特定的标签和属性。
  • 使用 Markdown:将用户输入的 Markdown 转换为安全的 HTML,避免直接嵌入用户提供的 HTML。

这几种方法可以在保证安全性的前提下,允许一定程度的 HTML 内容显示。选择具体方法时,应根据应用场景和安全需求做出适当选择。


参考

  1. Golang模板引擎快速入门教程
  2. Go语言标准库之http/template
Licensed under CC BY-NC-SA 4.0
最后更新于 Jun 04, 2024 14:00 +0800