Aksi bawaan template cukup untuk menyusun struktur, tetapi cepat kita temui kebutuhan format yang tidak tersedia: angka rupiah dengan pemisah ribuan, tanggal berbahasa Indonesia, atau slug dari judul artikel. Mesin template tidak membiarkan kita menulis logika Go di dalam {{ }} — dan itu memang disengaja agar template tetap deklaratif. Jalur resminya adalah mendaftarkan fungsi Go sebagai custom function melalui template.FuncMap, lalu memanggilnya seperti fungsi biasa atau di dalam pipeline. Pada chapter ini kita akan membangun fungsi formatRupiah, memakainya dalam nota, lalu mengamati dua error nyata yang muncul bila kontraknya dilanggar.
Mendaftarkan Fungsi dengan FuncMap #
template.FuncMap adalah map dari nama fungsi (string di dalam template) ke fungsi Go mana pun. Fungsinya boleh punya beberapa parameter, boleh menerima tipe apa pun, dan boleh mengembalikan satu nilai, atau dua nilai dengan elemen kedua bertipe error — dua bentuk itulah satu-satunya kontrak yang diakui mesin. Pemanggilan di template memakai argumen posisi: {{potongan .Total 10}} sama dengan pemanggilan potongan(data.Total, 10) di Go.
Program Utuh: formatRupiah dalam Nota #
Berkas functions-demo.go mendaftarkan tiga fungsi — strings.ToUpper langsung dari standard library, formatRupiah buatan sendiri, dan potongan yang mengembalikan error — lalu mengujinya pada tiga skenario termasuk dua skenario gagal:
package main
import (
"bytes"
"fmt"
"strconv"
"strings"
"text/template"
)
// formatRupiah memformat bilangan bulat dengan pemisah ribuan,
// contoh: 1234567 menjadi "1.234.567".
func formatRupiah(n int) string {
s := strconv.Itoa(n)
var b strings.Builder
awal := len(s) % 3
if awal > 0 {
b.WriteString(s[:awal])
}
for i := awal; i < len(s); i += 3 {
if b.Len() > 0 {
b.WriteByte('.')
}
b.WriteString(s[i : i+3])
}
return b.String()
}
// potongan menghitung nilai diskon; error dikembalikan bila
// persen berada di luar rentang yang masuk akal.
func potongan(total, persen int) (int, error) {
if persen < 0 || persen > 100 {
return 0, fmt.Errorf("persen %d di luar rentang 0-100", persen)
}
return total * persen / 100, nil
}
// Pembelian adalah data yang dirender menjadi nota singkat.
type Pembelian struct {
NamaToko string
Pelanggan string
Member bool
Items []string
Total int
}
func main() {
fm := template.FuncMap{
"upper": strings.ToUpper,
"formatRupiah": formatRupiah,
"potongan": potongan,
}
const nota = `TOKO : {{upper .NamaToko}}
PELANGGAN: {{.Pelanggan}}
ITEM : {{len .Items}} buah ({{index .Items 0}} dst)
TOTAL : {{.Total | formatRupiah}}
DISKON : {{potongan .Total 10 | formatRupiah}}
STATUS : {{if .Member}}MEMBER{{else}}UMUM{{end}}`
// Funcs wajib dipanggil SEBELUM Parse agar nama fungsi
// dikenali saat string template diurai.
t := template.Must(template.New("nota").Funcs(fm).Parse(nota))
data := Pembelian{
NamaToko: "Toko Contoh",
Pelanggan: "Siti Rahma",
Member: true,
Items: []string{"Buku Go", "Mug", "Stiker"},
Total: 1234567,
}
var buf bytes.Buffer
if err := t.Execute(&buf, data); err != nil {
panic(err)
}
fmt.Println(buf.String())
fmt.Println()
// Fungsi dua-nilai: error yang dikembalikan menghentikan eksekusi.
const salah = `DISKON: {{potongan .Total 150 | formatRupiah}}`
t2 := template.Must(template.New("nota-salah").Funcs(fm).Parse(salah))
var buf2 bytes.Buffer
err := t2.Execute(&buf2, data)
if err != nil {
fmt.Println("eksekusi gagal:", err)
}
// Nama fungsi divalidasi saat Parse, bukan saat Execute:
// template ini diurai tanpa pemanggilan Funcs lebih dulu.
_, err3 := template.New("terlambat").Parse(`{{upper .NamaToko}}`)
fmt.Println("parse gagal:", err3)
}Output yang Dihasilkan:
TOKO : TOKO CONTOH
PELANGGAN: Siti Rahma
ITEM : 3 buah (Buku Go dst)
TOTAL : 1.234.567
DISKON : 123.456
STATUS : MEMBER
eksekusi gagal: template: nota-salah:1:10: executing "nota-salah" at <potongan .Total 150>: error calling potongan: persen 150 di luar rentang 0-100
parse gagal: template: terlambat:1: function "upper" not definedMari kita telaah baris demi baris program di atas:
- Baris 13-27 (
func formatRupiah): algoritma pemisah ribuan bekerja murni pada string —strconv.Itoamengubah angka menjadi teks,strings.Buildermenyusun hasilnya dengan titik setiap tiga digit dari kanan; fungsi ini tidak berinteraksi dengan template sama sekali. - Baris 31-36 (
func potongan): bentuk dua-nilai(int, error)memenuhi kontrak fungsi template; nilai error yang bukannilakan diteruskan mesin template sebagai kegagalan eksekusi. - Baris 48-52 (
fm := template.FuncMap{...}): nama"upper"di map boleh berbeda dari nama fungsi Go-nya; nilainya cukup merujuk fungsi apa pun yang cocok, termasukstrings.ToUpperyang tidak kita tulis sendiri. - Baris 56 (
{{len .Items}}dan{{index .Items 0}}): dua fungsi bawaan dipanggil langsung —lenmenghitung jumlah item,indexmengambil elemen slice pada posisi nol. - Baris 57-58 (pipeline):
{{.Total | formatRupiah}}mengirimTotalsebagai argumenformatRupiah;{{potongan .Total 10 | formatRupiah}}menunjukkan hasil fungsi berargumen tetap dapat masuk pipeline — outputpotonganmenjadi inputformatRupiah. - Baris 63 (
New().Funcs(fm).Parse(nota)): pemanggilanFuncsharus terjadi sebelumParsekarena parser memvalidasi setiap nama fungsi saat mengurai string template. - Baris 81-88 (eksekusi gagal):
potongandipanggil dengan persen 150; error yang dikembalikan menghentikan eksekusi tepat di aksi tersebut sehinggabuf2dibiarkan kosong dan pesan lengkap tercetak dari blokif err != nil. - Baris 92-93 (parse gagal): template yang sama diurai tanpa
Funcslebih dulu; parser langsung menolaknya denganfunction "upper" not defined— bukti validasi nama terjadi saat parsing, bukan saat eksekusi.
Pesan pada baris terakhir output layak dicermati: template: terlambat:1: function "upper" not defined. Nama template, nomor baris, dan nama fungsi yang hilang semuanya tercantum, sehingga kesalahan pendaftaran mudah dilacak.
Fungsi Bawaan Template #
Selain fungsi kustom, mesin template menyediakan fungsi bawaan yang selalu tersedia — beberapa sudah kita pakai di program di atas:
| Fungsi | Perilaku |
|---|---|
and, or | Gabungan logika dengan evaluasi malas, mengembalikan argumen penentu hasil |
not | Negasi nilai kebenaran satu argumen |
len | Panjang string, slice, map, atau channel |
index | Akses elemen slice/array dengan indeks, atau nilai map dengan kunci |
slice | Mengiris string atau slice dengan batas bawah dan atas |
print, printf, println | Padanan fmt.Sprint, fmt.Sprintf, fmt.Sprintln |
call | Memanggil nilai bertipe fungsi yang datang dari data |
html, js, urlquery | Pengamanan escape ringkas untuk output teks |
Fungsi kustom membuat format data berpindah ke satu tempat yang dapat diuji — logika formatRupiah di atas bisa langsung dites sebagai fungsi Go biasa tanpa template. Kombinasi FuncMap dan escaping otomatis menyisakan satu pertanyaan penting: apa sebenarnya yang dilakukan html/template pada angka, tanda kutip, dan karakter berbahaya di berbagai posisi markup? Mekanisme itulah fokus chapter berikutnya.
Referensi #
- Funcs pada text/template — kontrak tanda tangan fungsi dan contoh FuncMap.
- Actions pada text/template — aturan pemanggilan fungsi dan pipeline.
- Dokumentasi package html/template — FuncMap dan escaping pada template HTML.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
functions-demo.go
Unduh berkaspackage main
import (
"bytes"
"fmt"
"strconv"
"strings"
"text/template"
)
// formatRupiah memformat bilangan bulat dengan pemisah ribuan,
// contoh: 1234567 menjadi "1.234.567".
func formatRupiah(n int) string {
s := strconv.Itoa(n)
var b strings.Builder
awal := len(s) % 3
if awal > 0 {
b.WriteString(s[:awal])
}
for i := awal; i < len(s); i += 3 {
if b.Len() > 0 {
b.WriteByte('.')
}
b.WriteString(s[i : i+3])
}
return b.String()
}
// potongan menghitung nilai diskon; error dikembalikan bila
// persen berada di luar rentang yang masuk akal.
func potongan(total, persen int) (int, error) {
if persen < 0 || persen > 100 {
return 0, fmt.Errorf("persen %d di luar rentang 0-100", persen)
}
return total * persen / 100, nil
}
// Pembelian adalah data yang dirender menjadi nota singkat.
type Pembelian struct {
NamaToko string
Pelanggan string
Member bool
Items []string
Total int
}
func main() {
fm := template.FuncMap{
"upper": strings.ToUpper,
"formatRupiah": formatRupiah,
"potongan": potongan,
}
const nota = `TOKO : {{upper .NamaToko}}
PELANGGAN: {{.Pelanggan}}
ITEM : {{len .Items}} buah ({{index .Items 0}} dst)
TOTAL : {{.Total | formatRupiah}}
DISKON : {{potongan .Total 10 | formatRupiah}}
STATUS : {{if .Member}}MEMBER{{else}}UMUM{{end}}`
// Funcs wajib dipanggil SEBELUM Parse agar nama fungsi
// dikenali saat string template diurai.
t := template.Must(template.New("nota").Funcs(fm).Parse(nota))
data := Pembelian{
NamaToko: "Toko Contoh",
Pelanggan: "Siti Rahma",
Member: true,
Items: []string{"Buku Go", "Mug", "Stiker"},
Total: 1234567,
}
var buf bytes.Buffer
if err := t.Execute(&buf, data); err != nil {
panic(err)
}
fmt.Println(buf.String())
fmt.Println()
// Fungsi dua-nilai: error yang dikembalikan menghentikan eksekusi.
const salah = `DISKON: {{potongan .Total 150 | formatRupiah}}`
t2 := template.Must(template.New("nota-salah").Funcs(fm).Parse(salah))
var buf2 bytes.Buffer
err := t2.Execute(&buf2, data)
if err != nil {
fmt.Println("eksekusi gagal:", err)
}
// Nama fungsi divalidasi saat Parse, bukan saat Execute:
// template ini diurai tanpa pemanggilan Funcs lebih dulu.
_, err3 := template.New("terlambat").Parse(`{{upper .NamaToko}}`)
fmt.Println("parse gagal:", err3)
}