template
Tài liệu chính thức: template package - text/template - Go Packages
Thông thường chúng ta hay sử dụng hàm fmt.Sprintf để định dạng chuỗi, nhưng nó chỉ phù hợp xử lý chuỗi nhỏ, hơn nữa cần sử dụng động từ định dạng để chỉ định kiểu, không thể đặt tên tham số, không hỗ trợ xử lý trong tình huống phức tạp, đây chính là vấn đề mà template engine cần giải quyết, ví dụ như trang tĩnh HTML gắn trực tiếp vào backend cần dùng đến template engine. Cộng đồng có nhiều thư viện template engine bên thứ ba xuất sắc, như pongo2, sprig, jet, nhưng nhân vật chính mà bài viết này muốn trình bày là thư viện template engine tích hợp sẵn của Go text/template, trong phát triển thực tế thường dùng html/template, cái sau dựa trên cái trước và đã xử lý nhiều vấn đề bảo mật liên quan đến HTML, thông thường dùng cái trước là được, nếu liên quan đến xử lý template HTML thì khuyên dùng cái sau sẽ an toàn hơn.
Bắt đầu nhanh
Dưới đây là một ví dụ đơn giản về sử dụng template engine, như sau
package main
import (
"fmt"
"os"
"text/template"
)
func main() {
tmpl := `This is the first template string, {{ .message }}`
te, err := template.New("texTmpl").Parse(tmpl)
if err != nil {
fmt.Println(err)
return
}
data := map[string]any{
"message": "hello world!",
}
execErr := te.Execute(os.Stdout, data)
if execErr != nil {
fmt.Println(err)
}
}Đầu ra của code trên là
This is the first template string, hello world!Trong code ví dụ, tmpl là một chuỗi template, {{ .message }} trong chuỗi là tham số template của template engine. Trước tiên phân tích chuỗi template thông qua phương thức *Template.Parse,
func (t *Template) Parse(text string) (*Template, error)Sau khi phân tích thành công thì thông qua phương thức *Template.Execute áp dụng dữ liệu data vào template, cuối cùng xuất ra Writer được truyền vào tức là os.Stdout.
func (t *Template) Execute(wr io.Writer, data any) errorTrong việc sử dụng template engine sau này, về cơ bản là ba bước này:
- Lấy template
- Phân tích template
- Áp dụng dữ liệu vào template
Có thể thấy việc sử dụng template engine thực ra khá đơn giản, phức tạp hơn một chút là cú pháp template của template engine, đây mới là nội dung chính mà bài viết này muốn giải thích.
Cú pháp template
Tham số
Go thông qua hai cặp dấu ngoặc nhọn {{ }} để biểu thị đây là tham số template trong template, thông qua . để biểu thị đối tượng gốc, đối tượng gốc chính là data được truyền vào. Giống như truy cập biến thành viên của một kiểu, thông qua ký hiệu . nối với tên biến là có thể truy cập giá trị tương ứng trong template, ví dụ
{{ .data }}Điều kiện là biến thành viên cùng tên phải tồn tại, nếu không sẽ báo lỗi. Đối với data được truyền vào, thông thường là struct hoặc map, cũng có thể là kiểu cơ bản, ví dụ số chuỗi, lúc này . đại diện cho đối tượng gốc chính là bản thân nó. Trong dấu ngoặc nhọn, không nhất thiết phải truy cập đối tượng gốc để lấy giá trị, cũng có thể là literal kiểu cơ bản, ví dụ
{{ 1 }}
{{ 3.14 }}
{{ "jack" }}Bất kể kiểu gì, cuối cùng đều thông qua fmt.Sprintf("%s", val) để lấy biểu diễn chuỗi của nó, xem ví dụ dưới đây.
func main() {
out := os.Stdout
tmpl := "data-> {{ . }}\n"
datas := []any{
"hello world!",
6379,
3.1415926,
[]any{1, "2*2", 3.6},
map[string]any{"data": "hello world!"},
struct {
Data string
}{Data: "hello world!"},
}
for _, data := range datas {
err := ExecTmpl(out, tmpl, data)
if err != nil {
panic(err)
}
}
}
func ExecTmpl(writer io.Writer, tmpl string, data any) error {
parsedTmpl, err := template.New("template").Parse(tmpl)
if err != nil {
return err
}
return parsedTmpl.Execute(writer, data)
}Đầu ra như sau
data-> hello world!
data-> 6379
data-> 3.1415926
data-> [1 2*2 3.6]
data-> map[data:hello world!]
data-> {hello world!}Có thể thấy hình thức đầu ra giống với việc直接使用 fmt.Sprintf. Đối với struct và map, có thể truy cập giá trị thông qua tên trường, như sau
func main() {
out := os.Stdout
tmpl := "data-> {{ .Data }}\n"
datas := []any{
map[string]any{"Data": "hello world!"},
struct {
Data string
}{Data: "hello world!"},
}
for _, data := range datas {
err := ExecTmpl(out, tmpl, data)
if err != nil {
panic(err)
}
}
}Đầu ra như sau
data-> hello world!
data-> hello world!Đối với slice và map, tuy không cung cấp cú pháp đặc biệt để truy cập giá trị của một chỉ số nào đó, nhưng có thể thực hiện thông qua gọi hàm, như sau
func main() {
out := os.Stdout
tmpl := "data-> {{ index . 1}}\n"
datas := []any{
[]any{"first", "second"},
map[int]any{1: "first"},
}
for _, data := range datas {
err := ExecTmpl(out, tmpl, data)
if err != nil {
panic(err)
}
}
}Đầu ra
data-> second
data-> firstNếu là slice đa chiều, có thể truy cập giá trị của chỉ số tương ứng thông qua cách sau, tương đương với s[i][j][k]
{{ index . i j k }}Đối với struct hoặc map lồng nhau, có thể sử dụng cách .k1.k2.k3 để truy cập, ví dụ
{{ .person.father.name }}Khi sử dụng tham số template, có thể thêm ký hiệu - trước và sau tham số để loại bỏ khoảng trắng trước và sau tham số, xem ví dụ
func main() {
out := os.Stdout
tmpl := `{{ .x }} {{ - .op - }} {{ .y }}`
datas := []any{
map[string]any{"x": "10", "op": ">", "y": "2"},
}
for _, data := range datas {
err := ExecTmpl(out, tmpl, data)
if err != nil {
panic(err)
}
}
}Bình thường kết quả đầu ra phải là 10 > 2, nhưng do đã thêm ký hiệu - trước và sau tham số op, nên khoảng trắng trước và sau nó đều bị loại bỏ, vì vậy đầu ra thực tế là
10>2Cần lưu ý là trong dấu ngoặc nhọn, ký hiệu - và tham số phải cách nhau một khoảng trắng, tức là phải là định dạng {{- . -}}, trong ví dụ sở dĩ thêm khoảng trắng ở hai bên viết thành định dạng {{ - . - }} thuần túy là cảm thấy nhìn thuận mắt hơn, thực tế không có hạn chế cú pháp này.
Chú thích
Cú pháp template hỗ trợ chú thích, chú thích sẽ không được tạo ra trong template cuối cùng, cú pháp như sau
{{/* this is a comment */}}Ký hiệu chú thích /* và */ phải liền kề với dấu ngoặc nhọn, giữa chúng không được có ký tự khác, nếu không sẽ không thể phân tích bình thường. Chỉ có một trường hợp ngoại lệ, đó là khi loại bỏ khoảng trắng
{{- /* this is a comment */ -}}Biến
Trong template cũng có thể khai báo biến, thông qua ký hiệu $ để biểu thị đây là biến, và thông qua := để gán giá trị, giống như code Go, ví dụ như sau.
{{ $name := .Name }}
{{ $val := index . 1 }}
{{ $val := index .dict key }}
// Gán giá trị số nguyên
{{ $numer := 1 }}
// Gán giá trị số dấu phẩy động
{{ $float := 1.234}}
// Gán giá trị chuỗi
{{ $name := "jack" }}Khi sử dụng sau này, truy cập giá trị của biến thông qua $ nối với tên biến, ví dụ
func main() {
out := os.Stdout
tmpl := `{{ $name := .name }} {{- $name }}`
datas := []any{
map[string]any{"name": "jack"},
}
for _, data := range datas {
err := ExecTmpl(out, tmpl, data)
if err != nil {
panic(err)
}
}
}Đầu ra
jackBiến phải được khai báo rồi mới có thể sử dụng, nếu không sẽ thông báo undefined variable, và cũng phải trong phạm vi mới có thể sử dụng.
Hàm
Cú pháp tự thân của template thực ra không nhiều, hầu hết chức năng đều được thực hiện thông qua hàm, định dạng gọi hàm là tên hàm sau đó nối với danh sách tham số, dùng khoảng trắng làm dấu phân cách, như sau
{{ funcname arg1 arg2 arg3 ... }}Ví dụ như hàm index đã dùng trước đó
{{ index .s 1 }}Hàm eq dùng để so sánh xem có bằng nhau không
{{ eq 1 2 }}Mỗi *Template đều có một FuncsMap, dùng để ghi lại ánh xạ hàm
type FuncMap map[string]anyKhi tạo template thì lấy bảng ánh xạ hàm mặc định từ text/template.builtins, dưới đây là tất cả các hàm tích hợp
| Tên hàm | Tác dụng | Ví dụ |
|---|---|---|
and | Phép AND | {{ and true false }} |
or | Phép OR | {{ or true false }} |
not | Phép NOT | {{ not true }} |
eq | Có bằng nhau không | {{ eq 1 2 }} |
ne | Có không bằng nhau không | {{ ne 1 2 }} |
lt | Nhỏ hơn | {{ lt 1 2 }} |
le | Nhỏ hơn hoặc bằng | {{ le 1 2 }} |
gt | Lớn hơn | {{ gt 1 2 }} |
ge | Lớn hơn hoặc bằng | {{ ge 1 2 }} |
len | Trả về độ dài | {{ len .slice }} |
index | Lấy phần tử ở chỉ số chỉ định của đối tượng | {{ index . 0 }} |
slice | Slice, tương đương với s[1:2:3] | {{ slice . 1 2 3 }} |
html | Escape HTML | {{ html .name }} |
js | Escape js | {{ js .name }} |
print | fmt.Sprint | {{ print . }} |
printf | fmt.Sprintf | {{ printf "%s" .}} |
println | fmt.Sprintln | {{ println . }} |
urlquery | Escape url query | {{ urlquery .query }} |
Ngoài những cái này ra, còn có một hàm tích hợp đặc biệt hơn là call, nó dùng để gọi trực tiếp hàm trong data được truyền vào trong thời kỳ Execute, ví dụ template dưới đây
{{ call .string 1024 }}Dữ liệu truyền vào như sau
map[string]any{
"string": func(val any) string { return fmt.Sprintf("%v: 2048", val) },
}Thì trong template sẽ tạo ra
1024: 2048Đây là một trong những cách tùy chỉnh hàm, nhưng thông thường khuyên dùng phương thức *Template.Funcs để thêm hàm tùy chỉnh, vì cái sau có thể tác dụng toàn cục, không cần绑定 vào đối tượng gốc.
func (t *Template) Funcs(funcMap FuncMap) *TemplateGiá trị trả về của hàm tùy chỉnh thông thường có hai cái, cái đầu tiên là giá trị trả về cần dùng đến, cái thứ hai là error. Ví dụ có hàm tùy chỉnh như sau
template.FuncMap{
"add": func(val any) (string, error) { return fmt.Sprintf("%v+1", val), nil },
}Sau đó sử dụng trực tiếp trong template
{{ add 1024 }}Kết quả của nó là
1024 + 1Pipeline
Pipeline này và chan là hai thứ khác nhau, trong tài liệu chính thức gọi nó là pipeline, bất kỳ thao tác nào có thể tạo ra dữ liệu đều được gọi là pipeline. Các thao tác template dưới đây đều thuộc về thao tác pipeline
{{ 1 }}
{{ eq 1 2 }}
{{ $name }}
{{ .name }}Ai quen thuộc linux chắc đều biết toán tử pipeline |, template cũng hỗ trợ cách viết như vậy. Thao tác pipeline thường xuất hiện trong template, ví dụ
{{ $name := 1 }}{{ $name | print | printf "%s+1=?" }}Kết quả của nó là
1+1=?Trong with, if, range sau này cũng sẽ thường xuyên dùng đến.
with
Thông qua câu lệnh with có thể kiểm soát phạm vi của biến và đối tượng gốc, định dạng như sau
{{ with pipeline }}
text
{{ end }}with sẽ kiểm tra giá trị trả về của thao tác pipeline, nếu giá trị为空 thì template text ở giữa sẽ không được tạo ra. Nếu muốn xử lý trường hợp rỗng, có thể sử dụng with else, định dạng như sau
{{ with pipeline }}
text1
{{ else }}
text2
{{ end }}Nếu giá trị trả về của thao tác pipeline为空, thì sẽ thực hiện logic của khối else này. Biến được khai báo trong câu lệnh with, phạm vi của nó chỉ giới hạn trong câu lệnh with, xem một ví dụ dưới đây
{{ $name := "mike" }}
{{ with $name := "jack" }}
{{- $name -}}
{{ end }}
{{- $name -}}Đầu ra của nó như sau, rõ ràng là do phạm vi khác nhau, chúng là hai biến khác nhau.
jackmikeThông qua câu lệnh with còn có thể viết lại đối tượng gốc trong phạm vi, như sau
{{ with .name }}
name: {{- .second }}-{{ .first -}}
{{ end }}
age: {{ .age }}
address: {{ .address }}Truyền vào dữ liệu như sau
map[string]any{
"name": map[string]any{
"first": "jack",
"second": "bob",
},
"age": 1,
"address": "usa",
}Đầu ra của nó
name:bob-jack
age: 1
address: usaCó thể thấy bên trong câu lệnh with, đối tượng gốc . đã biến thành .name.
Điều kiện
Định dạng của câu lệnh điều kiện như sau
{{ if pipeline }}
text1
{{ else if pipeline }}
text2
{{ else }}
text3
{{ end }}Giống như viết code bình thường, rất dễ hiểu. Dưới đây xem một vài ví dụ đơn giản,
{{ if eq .lang "en" }}
{{- .content.en -}}
{{ else if eq .lang "zh" }}
{{- .content.zh -}}
{{ else }}
{{- .content.fallback -}}
{{ end }}Dữ liệu truyền vào
map[string]any{
"lang": "zh",
"content": map[string]any{
"en": "hello, world!",
"zh": "你好,世界!",
"fallback": "hello, world!",
},
}Template trong ví dụ quyết định dựa trên ngôn ngữ lang được truyền vào để hiển thị nội dung theo cách nào, kết quả đầu ra
你好,世界!Lặp
Định dạng của câu lệnh lặp như sau, pipeline mà range hỗ trợ phải là mảng, slice, map, và channel.
{{ range pipeline }}
loop body
{{ end }}Kết hợp sử dụng với else, khi độ dài bằng 0 thì sẽ thực hiện nội dung khối else.
{{ range pipeline }}
loop body
{{ else }}
fallback
{{ end }}Ngoài ra, còn hỗ trợ các thao tác như break, continue, ví dụ
{{ range pipeline }}
{{ if pipeline }}
{{ break }}
{{ end }}
{{ if pipeline }}
{{ continue }}
{{ end }}
loop body
{{ end }}Dưới đây xem một ví dụ về lặp.
{{ range $index, $val := . }}
{{- if eq $index 0 }}
{{- continue -}}
{{ end -}}
{{- $index}}: {{ $val }}
{{ end }}Dữ liệu truyền vào
[]any{1, "2", 3.14},Đầu ra
1: 2
2: 3.14Lặp map cũng tương tự.
Lồng nhau
Trong một template có thể định nghĩa nhiều template, ví dụ
{{ define "t1" }} t1 {{ end }}
{{ define "t2" }} t2 {{ end }}Các template được định nghĩa này sẽ không được tạo ra trong template cuối cùng, trừ khi khi tải chỉ định tên hoặc thông qua câu lệnh template chỉ định thủ công.
func (t *Template) ExecuteTemplate(wr io.Writer, name string, data any) errorVí dụ như ví dụ dưới đây
{{ define "t1" }}
{{- with .t1 }}
{{- .data -}}
{{ end -}}
{{ end }}
{{ define "t2" }}
{{- with .t2 }}
{{- .data -}}
{{ end}}
{{ end -}}Truyền vào dữ liệu như sau
map[string]any{
"t1": map[string]any{"data": "template body 1"},
"t2": map[string]any{"data": "template body 2"},
}Code
func main() {
out := os.Stdout
tmpl :=
`{{ define "t1" }}
{{- with .t1 }}
{{- .data -}}
{{ end -}}
{{ end }}
{{ define "t2" }}
{{- with .t2 }}
{{- .data -}}
{{ end}}
{{ end -}}`
datas := []any{
map[string]any{
"t1": map[string]any{"data": "template body 1"},
"t2": map[string]any{"data": "template body 2"},
},
}
name := "t1"
for _, data := range datas {
err := ExecTmpl(out, tmpl, name, data)
if err != nil {
panic(err)
}
}
}
func ExecTmpl(writer io.Writer, tmpl string, name string, data any) error {
t := template.New("template")
parsedTmpl, err := t.Parse(tmpl)
if err != nil {
return err
}
return parsedTmpl.ExecuteTemplate(writer, name, data)
}Đầu ra
template body 1Hoặc cũng có thể chỉ định template thủ công
{{ define "t1" }}
{{- with .t1 }}
{{- .data -}}
{{ end -}}
{{ end }}
{{ define "t2" }}
{{- with .t2 }}
{{- .data -}}
{{ end}}
{{ end -}}
{{ template "t2" .}}Thì khi phân tích có chỉ định tên template hay không, t2 đều sẽ được tải.
Liên kết
Template con chỉ là khai báo nhiều template có tên bên trong một template, liên kết là liên kết nhiều *Template có tên bên ngoài lại với nhau. Sau đó thông qua câu lệnh template để tham chiếu đến template chỉ định.
{{ template "templateName" pipeline}}pipeline có thể chỉ định đối tượng gốc của template liên kết dựa trên nhu cầu của mình, hoặc cũng có thể truyền trực tiếp đối tượng gốc của template hiện tại. Xem một ví dụ code dưới đây
func main() {
tmpl1 := `name: {{ .name }}`
tmpl2 := `age: {{ .age }}`
tmpl3 := `Person Info
{{template "t1" .}}
{{template "t2" .}}`
t1, err := template.New("t1").Parse(tmpl1)
if err != nil {
panic(err)
}
t2, err := template.New("t2").Parse(tmpl2)
if err != nil {
panic(err)
}
t3, err := template.New("t3").Parse(tmpl3)
if err != nil {
panic(err)
}
if err := associate(t3, t1, t2); err != nil {
panic(err)
}
err = t3.Execute(os.Stdout, map[string]any{
"name": "jack",
"age": 18,
})
if err != nil {
panic(err)
}
}
func associate(t *template.Template, ts ...*template.Template) error {
for _, tt := range ts {
_, err := t.AddParseTree(tt.Name(), tt.Tree)
if err != nil {
return err
}
}
return nil
}Trong code trên, t3 liên kết với t1 và t2, sử dụng phương thức *Template.AddParseTree để liên kết
func (t *Template) AddParseTree(name string, tree *parse.Tree) (*Template, error)Kết quả tạo template cuối cùng là
Person Info
name: jack
age: 18Slot
Thông qua câu lệnh block, có thể thực hiện hiệu ứng slot giống như vue, mục đích của nó là để tái sử dụng một template nào đó. Xem một ví dụ sử dụng là biết cách dùng, định nghĩa slot trong template t1
Basic Person Info
name: {{ .name }}
age: {{ .age }}
address: {{ .address }}
{{ block "slot" . }} default content body {{ end }}Câu lệnh block có thể có nội dung mặc định của slot, khi các template khác sử dụng slot sau này, sẽ ghi đè nội dung mặc định. Trong template t2 tham chiếu template t1, và sử dụng define để định nghĩa nội dung nhúng
{{ template "person.txt" . }}
{{ define "slot" }}
school: {{ .school }}
{{ end }}Sau khi liên kết hai template, truyền vào dữ liệu như sau
map[string]any{
"name": "jack",
"age": 18,
"address": "usa",
"company": "google",
"school": "mit",
}Kết quả đầu ra cuối cùng là
Basic Person Info
name: jack
age: 18
address: usa
school: mitTệp template
Trong các ví dụ về cú pháp template, đều sử dụng literal chuỗi làm template, trong tình huống sử dụng thực tế hầu hết đều đặt template vào trong tệp.
func ParseFS(fsys fs.FS, patterns ...string) (*Template, error)Ví dụ template.ParseFs là tải template khớp với pattern từ hệ thống tệp chỉ định. Ví dụ dưới đây sử dụng embed.FS làm hệ thống tệp, chuẩn bị ba tệp
# person.txt
Basic Person Info
name: {{ .name }}
age: {{ .age }}
address: {{ .address }}
{{ block "slot" . }} {{ end }}
# student.txt
{{ template "person.txt" . }}
{{ define "slot" }}
school: {{ .school }}
{{ end }}
# employee.txt
{{ template "person.txt" . }}
{{ define "slot" }}
company: {{ .company }}
{{ end }}Code như sau
import (
"embed"
"os"
"text/template"
)
//go:embed *.txt
var fs embed.FS
func main() {
data := map[string]any{
"name": "jack",
"age": 18,
"address": "usa",
"company": "google",
"school": "mit",
}
t1, err := template.ParseFS(fs, "person.txt", "student.txt")
if err != nil {
panic(err)
}
t1.Execute(os.Stdout, data)
t2, err := template.ParseFS(fs, "person.txt", "employee.txt")
if err != nil {
panic(err)
}
t2.Execute(os.Stdout, data)
}Đầu ra là
Basic Person Info
name: jack
age: 18
address: usa
school: mit
Basic Person Info
name: jack
age: 18
address: usa
company: googleĐây là một ví dụ sử dụng tệp template rất đơn giản, person.txt làm tệp slot, hai cái khác tái sử dụng nội dung của nó và nhúng nội dung mới tùy chỉnh. Cũng có thể sử dụng hai hàm dưới đây
func ParseGlob(pattern string) (*Template, error)
func ParseFiles(filenames ...string) (*Template, error)ParseGlob dựa trên khớp ký tự đại diện, ParseFiles dựa trên tên tệp, chúng đều sử dụng hệ thống tệp cục bộ. Nếu là dùng để hiển thị tệp html ở frontend, khuyên dùng gói html/template, API mà nó cung cấp hoàn toàn giống với text/template, nhưng đã xử lý bảo mật cho html, css, js.
