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:

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

text
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 defined

Mari kita telaah baris demi baris program di atas:

  1. Baris 13-27 (func formatRupiah): algoritma pemisah ribuan bekerja murni pada string — strconv.Itoa mengubah angka menjadi teks, strings.Builder menyusun hasilnya dengan titik setiap tiga digit dari kanan; fungsi ini tidak berinteraksi dengan template sama sekali.
  2. Baris 31-36 (func potongan): bentuk dua-nilai (int, error) memenuhi kontrak fungsi template; nilai error yang bukan nil akan diteruskan mesin template sebagai kegagalan eksekusi.
  3. 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, termasuk strings.ToUpper yang tidak kita tulis sendiri.
  4. Baris 56 ({{len .Items}} dan {{index .Items 0}}): dua fungsi bawaan dipanggil langsung — len menghitung jumlah item, index mengambil elemen slice pada posisi nol.
  5. Baris 57-58 (pipeline): {{.Total | formatRupiah}} mengirim Total sebagai argumen formatRupiah; {{potongan .Total 10 | formatRupiah}} menunjukkan hasil fungsi berargumen tetap dapat masuk pipeline — output potongan menjadi input formatRupiah.
  6. Baris 63 (New().Funcs(fm).Parse(nota)): pemanggilan Funcs harus terjadi sebelum Parse karena parser memvalidasi setiap nama fungsi saat mengurai string template.
  7. Baris 81-88 (eksekusi gagal): potongan dipanggil dengan persen 150; error yang dikembalikan menghentikan eksekusi tepat di aksi tersebut sehingga buf2 dibiarkan kosong dan pesan lengkap tercetak dari blok if err != nil.
  8. Baris 92-93 (parse gagal): template yang sama diurai tanpa Funcs lebih dulu; parser langsung menolaknya dengan function "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:

FungsiPerilaku
and, orGabungan logika dengan evaluasi malas, mengembalikan argumen penentu hasil
notNegasi nilai kebenaran satu argumen
lenPanjang string, slice, map, atau channel
indexAkses elemen slice/array dengan indeks, atau nilai map dengan kunci
sliceMengiris string atau slice dengan batas bawah dan atas
print, printf, printlnPadanan fmt.Sprint, fmt.Sprintf, fmt.Sprintln
callMemanggil nilai bertipe fungsi yang datang dari data
html, js, urlqueryPengamanan 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 #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

functions-demo.go

Unduh berkas
go
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)
}