template
Dokumentasi resmi: template package - text/template - Go Packages
Saat ini kita sering menggunakan fungsi fmt.Sprintf untuk memformat string, tetapi hanya cocok untuk menangani string kecil, dan perlu menggunakan verb format untuk menentukan tipe, tidak dapat melakukan penamaan parameter, tidak mendukung penanganan situasi kompleks, dan inilah masalah yang perlu diselesaikan oleh mesin template, misalnya halaman HTML statis yang langsung ditautkan ke backend perlu menggunakan mesin template. Komunitas memiliki banyak library mesin template pihak ketiga yang sangat baik, seperti pongo2, sprig, jet, tetapi artikel ini akan membahas mesin template bawaan Go text/template, dalam pengembangan umum yang digunakan adalah html/template, yang terakhir didasarkan pada yang pertama dan melakukan banyak penanganan keamanan terkait HTML, dalam situasi umum menggunakan yang pertama sudah cukup, jika melibatkan penanganan template HTML disarankan menggunakan yang latter akan lebih aman.
Memulai
Berikut adalah contoh penggunaan sederhana mesin template, seperti yang ditunjukkan di bawah ini
package main
import (
"fmt"
"os"
"text/template"
)
func main() {
tmpl := `Ini adalah string template pertama, {{ .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)
}
}Output kode di atas adalah
Ini adalah string template pertama, hello world!Dalam kode kasus, tmpl adalah string template, {{ .message }} dalam string adalah parameter template mesin template. Pertama-tama parse string template melalui metode *Template.Parse,
func (t *Template) Parse(text string) (*Template, error)Setelah parsing berhasil, terapkan data data ke template melalui metode *Template.Execute, akhirnya output ke Writer yang diteruskan yaitu os.Stdout.
func (t *Template) Execute(wr io.Writer, data any) errorDi masa mendatang, penggunaan mesin template pada dasarnya terdiri dari tiga langkah ini:
- Mendapatkan template
- Parsing template,
- Menerapkan data ke template
Dapat dilihat bahwa penggunaan mesin template cukup sederhana, yang sedikit lebih kompleks adalah sintaks template mesin template, inilah konten utama yang akan dijelaskan dalam artikel ini.
Sintaks Template
Parameter
Go menggunakan dua pasang kurung kurawal {{ }} untuk menunjukkan ini adalah parameter template dalam template, menggunakan . untuk menunjukkan objek root, objek root adalah data yang diteruskan. Seperti mengakses variabel anggota tipe, menggunakan simbol . untuk menyambungkan nama variabel dapat mengakses nilai yang sesuai dalam template, misalnya
{{ .data }}Prasyaratnya adalah variabel anggota dengan nama yang sama ada, jika tidak akan terjadi error. Untuk data yang diteruskan, umumnya struct atau map, juga bisa berupa tipe dasar, seperti angka string, pada saat ini . yang mewakili objek root adalah dirinya sendiri. Dalam kurung kurawal, tidak harus mengakses objek root untuk mendapatkan nilai, juga bisa berupa literal tipe dasar, misalnya
{{ 1 }}
{{ 3.14 }}
{{ "jack" }}Apa pun tipenya, pada akhirnya akan mendapatkan representasi stringnya melalui fmt.Sprintf("%s", val), lihat contoh di bawah ini.
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)
}Output sebagai berikut
data-> hello world!
data-> 6379
data-> 3.1415926
data-> [1 2*2 3.6]
data-> map[data:hello world!]
data-> {hello world!}Dapat dilihat bahwa bentuk outputnya konsisten dengan直接使用 fmt.Sprintf. Untuk struct dan map, dapat mengakses nilainya melalui nama field, seperti yang ditunjukkan di bawah ini
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)
}
}
}Output sebagai berikut
data-> hello world!
data-> hello world!Untuk slice dan map, meskipun tidak menyediakan sintaks khusus untuk mengakses nilai indeks tertentu, tetapi dapat diimplementasikan melalui pemanggilan fungsi, seperti yang ditunjukkan di bawah ini
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)
}
}
}Output
data-> second
data-> firstJika slice multidimensi, dapat mengakses nilai indeks yang sesuai melalui cara berikut, setara dengan s[i][j][k]
{{ index . i j k }}Untuk struct atau map bersarang, dapat menggunakan cara .k1.k2.k3 untuk mengakses, misalnya
{{ .person.father.name }}Saat menggunakan parameter template, dapat menambahkan simbol - di depan dan belakang parameter untuk menghilangkan spasi di depan dan belakang parameter, lihat contoh
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)
}
}
}Seharusnya outputnya adalah 10 > 2, tetapi karena menambahkan simbol - di depan dan belakang parameter op, jadi spasi di depan dan belakangnya akan dihilangkan, jadi output sebenarnya adalah
10>2Perlu diperhatikan, dalam kurung kurawal, simbol - harus dipisahkan dengan spasi dari parameter, artinya harus dalam format {{- . -}}, dalam contoh alasan menambahkan spasi di kedua sisi dan menulis dalam format {{ - . - }} murni karena terlihat lebih enak dipandang, sebenarnya tidak ada batasan sintaks seperti ini.
Komentar
Sintaks template mendukung komentar, komentar tidak akan dihasilkan dalam template akhir, sintaksnya adalah sebagai berikut
{{/* ini adalah komentar */}}Simbol komentar /* dan */ harus berdekatan dengan kurung kurawal, tidak boleh ada karakter lain di antaranya, jika tidak tidak akan dapat diuraikan dengan benar. Hanya ada satu pengecualian, yaitu saat menghilangkan spasi
{{- /* ini adalah komentar */ -}}Variabel
Dalam template juga dapat mendeklarasikan variabel, menggunakan simbol $ untuk menunjukkan ini adalah variabel, dan menggunakan := untuk assignment, sama seperti kode Go, contohnya adalah sebagai berikut.
{{ $name := .Name }}
{{ $val := index . 1 }}
{{ $val := index .dict key }}
// Assignment integer
{{ $numer := 1 }}
// Assignment floating point
{{ $float := 1.234}}
// Assignment string
{{ $name := "jack" }}Saat penggunaan selanjutnya, mengakses nilai variabel tersebut melalui $ yang menyambungkan nama variabel, misalnya
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)
}
}
}Output
jackVariabel harus dideklarasikan terlebih dahulu sebelum digunakan, jika tidak akan menampilkan undefined variable, dan juga harus dalam cakupan agar dapat digunakan.
Fungsi
Sintaks template itu sendiri sebenarnya tidak banyak, sebagian besar fungsi diimplementasikan melalui fungsi, format pemanggilan fungsi adalah nama fungsi diikuti oleh daftar parameter, menggunakan spasi sebagai pemisah, seperti yang ditunjukkan di bawah ini
{{ funcname arg1 arg2 arg3 ... }}Misalnya fungsi index yang digunakan sebelumnya
{{ index .s 1 }}Fungsi eq untuk membandingkan apakah sama
{{ eq 1 2 }}Setiap *Template memiliki FuncsMap, digunakan untuk merekam pemetaan fungsi
type FuncMap map[string]anySaat membuat template, mendapatkan tabel pemetaan fungsi default dari text/template.builtins, di bawah ini adalah semua fungsi bawaan
| Nama Fungsi | Fungsi | Contoh |
|---|---|---|
and | Operasi AND | {{ and true false }} |
or | Operasi OR | {{ or true false }} |
not | Operasi NOT | {{ not true }} |
eq | Apakah sama | {{ eq 1 2 }} |
ne | Apakah tidak sama | {{ ne 1 2 }} |
lt | Kurang dari | {{ lt 1 2 }} |
le | Kurang dari atau sama dengan | {{ le 1 2 }} |
gt | Lebih dari | {{ gt 1 2 }} |
ge | Lebih dari atau sama dengan | {{ ge 1 2 }} |
len | Mengembalikan panjang | {{ len .slice }} |
index | Mendapatkan elemen indeks tertentu dari target | {{ index . 0 }} |
slice | Slice, setara dengan 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 }} |
Selain ini, ada fungsi bawaan yang cukup khusus call, digunakan untuk langsung memanggil fungsi dalam data yang diteruskan selama periode Execute, misalnya template berikut
{{ call .string 1024 }}Data yang diteruskan adalah sebagai berikut
map[string]any{
"string": func(val any) string { return fmt.Sprintf("%v: 2048", val) },
}Maka akan dihasilkan dalam template
1024: 2048Ini adalah salah satu cara untuk membuat fungsi kustom, tetapi biasanya disarankan menggunakan metode *Template.Funcs untuk menambahkan fungsi kustom, karena yang terakhir dapat berlaku global, tidak perlu mengikat ke objek root.
func (t *Template) Funcs(funcMap FuncMap) *TemplateNilai kembalian fungsi kustom umumnya ada dua, yang pertama adalah nilai kembalian yang diperlukan, yang kedua adalah error. Misalnya ada fungsi kustom berikut
template.FuncMap{
"add": func(val any) (string, error) { return fmt.Sprintf("%v+1", val), nil },
}Kemudian langsung digunakan dalam template
{{ add 1024 }}Hasilnya adalah
1024 + 1Pipeline
Pipeline ini berbeda dengan chan, dokumentasi resmi menyebutnya pipeline, setiap operasi yang dapat menghasilkan data disebut pipeline. Operasi template berikut termasuk operasi pipeline
{{ 1 }}
{{ eq 1 2 }}
{{ $name }}
{{ .name }}Yang familiar dengan linux seharusnya tahu operator pipeline |, template juga mendukung penulisan seperti ini. Operasi pipeline sering muncul dalam template, misalnya
{{ $name := 1 }}{{ $name | print | printf "%s+1=?" }}Hasilnya adalah
1+1=?Dalam with, if, range selanjutnya juga akan sering digunakan.
With
Melalui pernyataan with dapat mengontrol cakupan variabel dan objek root, formatnya adalah sebagai berikut
{{ with pipeline }}
text
{{ end }}with akan memeriksa nilai yang dikembalikan oleh operasi pipeline, jika nilainya kosong, maka template text di tengah tidak akan dihasilkan. Jika ingin menangani kasus kosong, dapat menggunakan with else, formatnya adalah sebagai berikut
{{ with pipeline }}
text1
{{ else }}
text2
{{ end }}Jika nilai yang dikembalikan oleh operasi pipeline kosong, maka akan mengeksekusi logika blok else ini. Variabel yang dideklarasikan dalam pernyataan with, cakupannya terbatas pada pernyataan with, lihat contoh di bawah ini
{{ $name := "mike" }}
{{ with $name := "jack" }}
{{- $name -}}
{{ end }}
{{- $name -}}Outputnya adalah sebagai berikut, jelas ini karena cakupan yang berbeda, mereka adalah dua variabel yang berbeda.
jackmikeMelalui pernyataan with juga dapat menulis ulang objek root dalam cakupan, seperti berikut
{{ with .name }}
name: {{- .second }}-{{ .first -}}
{{ end }}
age: {{ .age }}
address: {{ .address }}Melewati data berikut
map[string]any{
"name": map[string]any{
"first": "jack",
"second": "bob",
},
"age": 1,
"address": "usa",
}Outputnya
name:bob-jack
age: 1
address: usaDapat dilihat bahwa dalam pernyataan with, objek root . sudah berubah menjadi .name.
Kondisi
Format pernyataan kondisi adalah sebagai berikut
{{ if pipeline }}
text1
{{ else if pipeline }}
text2
{{ else }}
text3
{{ end }}Sama seperti menulis kode biasa, sangat mudah dipahami. Berikut beberapa contoh sederhana,
{{ if eq .lang "en" }}
{{- .content.en -}}
{{ else if eq .lang "zh" }}
{{- .content.zh -}}
{{ else }}
{{- .content.fallback -}}
{{ end }}Data yang diteruskan
map[string]any{
"lang": "zh",
"content": map[string]any{
"en": "hello, world!",
"zh": "你好,世界!",
"fallback": "hello, world!",
},
}Template dalam contoh menentukan cara menampilkan konten berdasarkan bahasa lang yang diteruskan, hasil output
你好,世界!Iterasi
Format pernyataan iterasi adalah sebagai berikut, range yang didukung harus berupa array, slice, map, dan channel.
{{ range pipeline }}
loop body
{{ end }}Digunakan bersama else, ketika panjangnya 0, akan mengeksekusi konten blok else.
{{ range pipeline }}
loop body
{{ else }}
fallback
{{ end }}Selain itu, juga mendukung operasi seperti break, continue, misalnya
{{ range pipeline }}
{{ if pipeline }}
{{ break }}
{{ end }}
{{ if pipeline }}
{{ continue }}
{{ end }}
loop body
{{ end }}Berikut adalah contoh iterasi.
{{ range $index, $val := . }}
{{- if eq $index 0 }}
{{- continue -}}
{{ end -}}
{{- $index}}: {{ $val }}
{{ end }}Melewati data
[]any{1, "2", 3.14},Output
1: 2
2: 3.14Iterasi map juga sama.
Nested
Dalam satu template dapat didefinisikan beberapa template, misalnya
{{ define "t1" }} t1 {{ end }}
{{ define "t2" }} t2 {{ end }}Template yang didefinisikan ini tidak akan dihasilkan dalam template akhir, kecuali saat memuat menentukan nama atau menentukan secara manual melalui pernyataan template.
func (t *Template) ExecuteTemplate(wr io.Writer, name string, data any) errorMisalnya contoh di bawah ini
{{ define "t1" }}
{{- with .t1 }}
{{- .data -}}
{{ end -}}
{{ end }}
{{ define "t2" }}
{{- with .t2 }}
{{- .data -}}
{{ end}}
{{ end -}}Melewati data berikut
map[string]any{
"t1": map[string]any{"data": "template body 1"},
"t2": map[string]any{"data": "template body 2"},
}Kode
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)
}Output
template body 1Atau juga dapat menentukan template secara manual
{{ define "t1" }}
{{- with .t1 }}
{{- .data -}}
{{ end -}}
{{ end }}
{{ define "t2" }}
{{- with .t2 }}
{{- .data -}}
{{ end}}
{{ end -}}
{{ template "t2" .}}Maka saat parsing apakah menentukan nama template atau tidak, t2 akan dimuat.
Asosiasi
Sub-template hanya mendeklarasikan beberapa template bernama dalam satu template internal, asosiasi adalah mengasosiasikan beberapa *Template bernama eksternal. Kemudian merujuk template yang ditentukan melalui pernyataan template.
{{ tempalte "templateName" pipeline}}pipeline dapat menentukan objek root template asosiasi sesuai kebutuhan, atau juga dapat langsung meneruskan objek root template saat ini. Lihat contoh kode di bawah ini
func main() {
tmpl1 := `name: {{ .name }}`
tmpl2 := `age: {{ .age }}`
tmpl3 := `Info Person
{{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
}Dalam kode di atas, t3 mengasosiasikan t1 dan t2, menggunakan metode *Template.AddParseTree untuk asosiasi
func (t *Template) AddParseTree(name string, tree *parse.Tree) (*Template, error)Hasil generasi template akhir adalah
Info Person
name: jack
age: 18Slot
Melalui pernyataan block, dapat mencapai efek seperti slot vue, tujuannya adalah untuk menggunakan kembali template tertentu. Lihat contoh penggunaan maka akan tahu cara menggunakannya, dalam template t1 mendefinisikan slot
Info Dasar Person
name: {{ .name }}
age: {{ .age }}
address: {{ .address }}
{{ block "slot" . }} konten default body {{ end }}Pernyataan block dapat menjadi konten default dalam slot, saat template lain menggunakan slot di masa mendatang, akan menimpa konten default. Dalam template t2 merujuk template t1, dan menggunakan define untuk mendefinisikan konten yang disematkan
{{ template "person.txt" . }}
{{ define "slot" }}
school: {{ .school }}
{{ end }}Setelah mengasosiasikan dua template, melewati data berikut
map[string]any{
"name": "jack",
"age": 18,
"address": "usa",
"company": "google",
"school": "mit",
}Hasil output akhir adalah
Info Dasar Person
name: jack
age: 18
address: usa
school: mitFile Template
Dalam kasus sintaks template, semuanya menggunakan literal string sebagai template, dalam situasi penggunaan sebenarnya sebagian besar template ditempatkan dalam file.
func ParseFS(fsys fs.FS, patterns ...string) (*Template, error)Misalnya template.ParseFs adalah memuat template yang cocok dengan pattern dari sistem file yang ditentukan. Contoh di bawah ini menggunakan embed.FS sebagai sistem file, menyiapkan tiga file
# person.txt
Info Dasar Person
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 }}Kode adalah sebagai berikut
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)
}Output adalah
Info Dasar Person
name: jack
age: 18
address: usa
school: mit
Info Dasar Person
name: jack
age: 18
address: usa
company: googleIni adalah contoh penggunaan file template yang sangat sederhana, person.txt sebagai file slot, dua lainnya menggunakan kembali kontennya dan menyematkan konten kustom baru. Juga dapat menggunakan dua fungsi di bawah ini
func ParseGlob(pattern string) (*Template, error)
func ParseFiles(filenames ...string) (*Template, error)ParseGlob berdasarkan pencocokan wildcard, ParseFiles berdasarkan nama file, keduanya menggunakan sistem file lokal. Jika digunakan untuk menampilkan file html di frontend, disarankan menggunakan package html/template, API yang disediakan konsisten dengan text/template, tetapi melakukan penanganan keamanan untuk html, css, js.
