Skip to content

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

go
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,

go
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.

go
func (t *Template) Execute(wr io.Writer, data any) error

Di masa mendatang, penggunaan mesin template pada dasarnya terdiri dari tiga langkah ini:

  1. Mendapatkan template
  2. Parsing template,
  3. 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.

go
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

go
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

go
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-> first

Jika 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

go
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>2

Perlu 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

go
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

jack

Variabel 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

go
type FuncMap map[string]any

Saat membuat template, mendapatkan tabel pemetaan fungsi default dari text/template.builtins, di bawah ini adalah semua fungsi bawaan

Nama FungsiFungsiContoh
andOperasi AND{{ and true false }}
orOperasi OR{{ or true false }}
notOperasi NOT{{ not true }}
eqApakah sama{{ eq 1 2 }}
neApakah tidak sama{{ ne 1 2 }}
ltKurang dari{{ lt 1 2 }}
leKurang dari atau sama dengan{{ le 1 2 }}
gtLebih dari{{ gt 1 2 }}
geLebih dari atau sama dengan{{ ge 1 2 }}
lenMengembalikan panjang{{ len .slice }}
indexMendapatkan elemen indeks tertentu dari target{{ index . 0 }}
sliceSlice, setara dengan s[1:2:3]{{ slice . 1 2 3 }}
htmlEscape HTML{{ html .name }}
jsEscape js{{ js .name }}
printfmt.Sprint{{ print . }}
printffmt.Sprintf{{ printf "%s" .}}
printlnfmt.Sprintln{{ println . }}
urlqueryEscape 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

go
map[string]any{
    "string": func(val any) string { return fmt.Sprintf("%v: 2048", val) },
}

Maka akan dihasilkan dalam template

1024: 2048

Ini 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.

go
func (t *Template) Funcs(funcMap FuncMap) *Template

Nilai kembalian fungsi kustom umumnya ada dua, yang pertama adalah nilai kembalian yang diperlukan, yang kedua adalah error. Misalnya ada fungsi kustom berikut

go
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 + 1

Pipeline

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.

jackmike

Melalui 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

go
map[string]any{
    "name": map[string]any{
        "first":  "jack",
        "second": "bob",
    },
    "age":     1,
    "address": "usa",
}

Outputnya

name:bob-jack
age: 1
address: usa

Dapat 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

go
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

go
[]any{1, "2", 3.14},

Output

1: 2
2: 3.14

Iterasi 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) error

Misalnya contoh di bawah ini

{{ define "t1" }}
    {{- with .t1 }}
      {{- .data -}}
    {{ end -}}
{{ end }}
{{ define "t2" }}
    {{- with .t2 }}
      {{- .data -}}
    {{ end}}
{{ end -}}

Melewati data berikut

go
map[string]any{
    "t1": map[string]any{"data": "template body 1"},
    "t2": map[string]any{"data": "template body 2"},
}

Kode

go
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 1

Atau 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

go
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

go
func (t *Template) AddParseTree(name string, tree *parse.Tree) (*Template, error)

Hasil generasi template akhir adalah

Info Person
name: jack
age: 18

Slot

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

go
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: mit

File Template

Dalam kasus sintaks template, semuanya menggunakan literal string sebagai template, dalam situasi penggunaan sebenarnya sebagian besar template ditempatkan dalam file.

go
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

txt
# 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

go
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: google

Ini 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

go
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.

Golang by www.golangdev.cn edit