Tidak semua output program adalah HTML. Faktur yang dicetak ke terminal, laporan bulanan yang disimpan ke berkas, dan badan email yang dikirim ke pelanggan semuanya teks biasa yang perlu digabungkan dari data. Package text/template adalah mesin template serbaguna untuk semua format tersebut — html/template yang kita bahas sebelumnya sebenarnya dibangun di atasnya dengan tambahan lapisan keamanan. Pada chapter ini kita akan menyusun faktur berkolom rapi, memangkas spasi berlebih dengan trim marker, lalu merender email dari struct yang sama.
Hubungan text/template dan html/template #
Kedua package memakai mesin dan sintaks yang identik: aksi {{.Field}}, {{range}}, {{if}}, pipeline, sampai {{define}} semuanya bekerja sama. Perbedaannya satu dan fundamental: text/template mencetak hasil apa adanya tanpa proses escaping. Tanda <, &, dan kutip tidak diubah menjadi entitas apa pun, karena target outputnya memang bukan halaman web. Perbandingannya ringkas:
| Aspek | text/template | html/template |
|---|---|---|
| Sintaks aksi | Identik | Identik |
| Escaping otomatis | Tidak ada | Sesuai konteks HTML |
| Target khas | Laporan, email, berkas teks, kode yang dihasilkan | Halaman dan fragmen HTML |
| Tipe penanda tepercaya | Tidak ada | template.HTML, template.JS, template.URL |
Laporan Faktur dengan printf #
Aksi {{printf}} membungkus fungsi fmt.Sprintf sehingga kita bisa merapikan kolom angka dan teks. Format %-18s meratakan nama item ke kiri selebar 18 karakter, %3d meratakan jumlah ke kanan selebar 3 karakter, dan Rp%-7d menjaga kolom subtotal tetap sejajar. Hasilnya terlihat pada laporan berkolom di program utuh di bawah — tanpa printf, angka dengan panjang berbeda akan membuat kolom bergerigi.
Memangkas Whitespace dengan Trim Marker #
Setiap aksi yang ditulis pada baris tersendiri meninggalkan baris baru di sekitarnya. Tanpa penanganan, laporan yang memakai {{range}} akan dipenuhi baris kosong. Solusinya adalah trim marker: tanda minus di sisi dalam kurung kurawal. Penulisan {{- range .Items}} memangkas spasi dan baris baru sebelum aksi, sedangkan {{- end}} memangkas teks sesudahnya. Sepasang {{- dan -}} membuat blok loop menyatu mulus dengan teks di sekelilingnya.
Program Utuh: Faktur dan Email #
Berkas text-demo.go merender dua dokumen dari satu struct Faktur: nota berkolom untuk diarsipkan dan email pemberitahuan. Bagian akhir program membuktikan bahwa tidak ada escaping yang berjalan:
package main
import (
"bytes"
"fmt"
"strings"
"text/template"
)
// Item adalah satu baris pembelian pada faktur.
type Item struct {
Nama string
Jumlah int
Harga int
}
// Subtotal menghitung harga satuan dikali jumlah.
func (it Item) Subtotal() int {
return it.Jumlah * it.Harga
}
// Faktur merangkum data tagihan seorang pelanggan.
type Faktur struct {
Nomor string
Pelanggan string
Items []Item
}
// Total menjumlahkan seluruh subtotal item.
func (f Faktur) Total() int {
total := 0
for _, it := range f.Items {
total += it.Subtotal()
}
return total
}
func main() {
// Tanda {{- dan -}} memangkas spasi serta baris baru
// di sekitar aksi agar laporan tidak berlubang baris kosong.
const nota = `FAKTUR {{.Nomor}} — Toko Contoh
Kepada: {{.Pelanggan}}
------------------------------------------
{{- range .Items}}
{{printf "%-18s %3d x Rp%-7d Rp%d" .Nama .Jumlah .Harga .Subtotal}}
{{- end}}
------------------------------------------
TOTAL: Rp{{.Total}}
`
f := Faktur{
Nomor: "INV-2026-001",
Pelanggan: "Budi Santoso",
Items: []Item{
{Nama: "Buku Go", Jumlah: 2, Harga: 95000},
{Nama: "Mug Keramik", Jumlah: 1, Harga: 45000},
{Nama: "Stiker Gopher", Jumlah: 5, Harga: 8000},
},
}
t := template.Must(template.New("nota").Parse(nota))
var buf bytes.Buffer
if err := t.Execute(&buf, f); err != nil {
panic(err)
}
fmt.Println(buf.String())
const email = `Dari: [email protected]
Kepada: {{.Pelanggan}}
Subjek: Faktur {{.Nomor}}
Halo {{.Pelanggan}},
Terima kasih atas pesanan Anda. Rincian pembelian:
{{- range .Items}}
- {{.Nama}} sejumlah {{.Jumlah}} pcs
{{- end}}
Total tagihan Rp{{.Total}} sudah kami terima.
Simpan email ini sebagai bukti pembelian.
`
t2 := template.Must(template.New("email").Parse(email))
var buf2 bytes.Buffer
if err := t2.Execute(&buf2, f); err != nil {
panic(err)
}
fmt.Println(buf2.String())
// text/template tidak melakukan escaping apa pun:
// markup dan tanda kutip dicetak apa adanya.
const cek = `Teks mentah: {{.}}`
t3 := template.Must(template.New("cek").Parse(cek))
var sb strings.Builder
if err := t3.Execute(&sb, `<b>tebal</b> & "kutip"`); err != nil {
panic(err)
}
fmt.Println(sb.String())
}Output yang Dihasilkan:
FAKTUR INV-2026-001 — Toko Contoh
Kepada: Budi Santoso
------------------------------------------
Buku Go 2 x Rp95000 Rp190000
Mug Keramik 1 x Rp45000 Rp45000
Stiker Gopher 5 x Rp8000 Rp40000
------------------------------------------
TOTAL: Rp275000
Dari: [email protected]
Kepada: Budi Santoso
Subjek: Faktur INV-2026-001
Halo Budi Santoso,
Terima kasih atas pesanan Anda. Rincian pembelian:
- Buku Go sejumlah 2 pcs
- Mug Keramik sejumlah 1 pcs
- Stiker Gopher sejumlah 5 pcs
Total tagihan Rp275000 sudah kami terima.
Simpan email ini sebagai bukti pembelian.
Teks mentah: <b>tebal</b> & "kutip"Mari kita telaah baris demi baris program di atas:
- Baris 18-20 (
func (it Item) Subtotal): mendeklarasikan method pada structItem; template dapat memanggil method tersebut seperti field, cukup dengan{{.Subtotal}}tanpa tanda kurung. - Baris 30-36 (
func (f Faktur) Total): methodTotaldijumlahkan dari seluruh subtotal; aksi{{.Total}}memanggilnya sekali setiap kali nilai dibutuhkan. - Baris 44-46 (
{{- range .Items}}…{{- end}}): trim marker memangkas baris kosong di sekitar badan loop sehingga baris item langsung menempel pada garis pemisah. - Baris 45 (
{{printf "%-18s ..." .Nama .Jumlah .Harga .Subtotal}}): aksiprintfmenerima empat argumen — method.Subtotalikut menjadi argumen biasa, bukti method dan field setara di mata template. - Baris 61-66 (parse dan execute): template diurai dengan
template.Mustlalu dieksekusi kebytes.Buffer; hasil akhirnya dicetak sekali melaluifmt.Println. - Baris 93-99 (template
cek): template satu aksi{{.}}dieksekusi dengan data string mentah berisi markup dan tanda kutip; outputnya tercetak persis sama tanpa satu entitas pun — perilaku inilah pembeda utama darihtml/template. - Baris 76-78 (loop pada email): daftar item tampil sebagai butir
- Nama sejumlah N pcsdengan trim marker yang sama, menunjukkan satu pola loop dipakai ulang pada dua dokumen berbeda.
Perhatikan dua detail pada output. Pertama, setiap baris nota memiliki kolom yang sejajar rapi meskipun panjang nama item berbeda — itu kerja format width %-18s dan %3d. Kedua, baris terakhir Teks mentah: <b>tebal</b> & "kutip" tercetak mentah; html/template akan mengubah string tersebut menjadi <b>tebal</b> di posisi yang sama.
Kapan Memilih text/template #
Pilih text/template setiap kali target render bukan halaman web: laporan operasional, badan email plain-text, berkas konfigurasi yang dihasilkan, sampai code generator yang mencetak sumber Go dari cetakan. Karena tidak ada lapisan keamanan, package ini juga sedikit lebih cepat dan hasilnya selalu dapat diprediksi persis — penting saat kita menguji output dengan bytes.Buffer.
Untuk laporan yang lebih panjang, satu template besar cepat menjadi sulit dirawat. Kita butuh cara memecahnya menjadi bagian-bagian kecil — header, footer, layout — yang dapat dipakai ulang; teknik komposisi itulah topik chapter berikutnya.
Referensi #
- Dokumentasi package text/template — referensi resmi API dan aturan sintaks.
- Actions pada text/template
— daftar aksi lengkap termasuk
printfdan trim marker.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
text-demo.go
Unduh berkaspackage main
import (
"bytes"
"fmt"
"strings"
"text/template"
)
// Item adalah satu baris pembelian pada faktur.
type Item struct {
Nama string
Jumlah int
Harga int
}
// Subtotal menghitung harga satuan dikali jumlah.
func (it Item) Subtotal() int {
return it.Jumlah * it.Harga
}
// Faktur merangkum data tagihan seorang pelanggan.
type Faktur struct {
Nomor string
Pelanggan string
Items []Item
}
// Total menjumlahkan seluruh subtotal item.
func (f Faktur) Total() int {
total := 0
for _, it := range f.Items {
total += it.Subtotal()
}
return total
}
func main() {
// Tanda {{- dan -}} memangkas spasi serta baris baru
// di sekitar aksi agar laporan tidak berlubang baris kosong.
const nota = `FAKTUR {{.Nomor}} — Toko Contoh
Kepada: {{.Pelanggan}}
------------------------------------------
{{- range .Items}}
{{printf "%-18s %3d x Rp%-7d Rp%d" .Nama .Jumlah .Harga .Subtotal}}
{{- end}}
------------------------------------------
TOTAL: Rp{{.Total}}
`
f := Faktur{
Nomor: "INV-2026-001",
Pelanggan: "Budi Santoso",
Items: []Item{
{Nama: "Buku Go", Jumlah: 2, Harga: 95000},
{Nama: "Mug Keramik", Jumlah: 1, Harga: 45000},
{Nama: "Stiker Gopher", Jumlah: 5, Harga: 8000},
},
}
t := template.Must(template.New("nota").Parse(nota))
var buf bytes.Buffer
if err := t.Execute(&buf, f); err != nil {
panic(err)
}
fmt.Println(buf.String())
const email = `Dari: [email protected]
Kepada: {{.Pelanggan}}
Subjek: Faktur {{.Nomor}}
Halo {{.Pelanggan}},
Terima kasih atas pesanan Anda. Rincian pembelian:
{{- range .Items}}
- {{.Nama}} sejumlah {{.Jumlah}} pcs
{{- end}}
Total tagihan Rp{{.Total}} sudah kami terima.
Simpan email ini sebagai bukti pembelian.
`
t2 := template.Must(template.New("email").Parse(email))
var buf2 bytes.Buffer
if err := t2.Execute(&buf2, f); err != nil {
panic(err)
}
fmt.Println(buf2.String())
// text/template tidak melakukan escaping apa pun:
// markup dan tanda kutip dicetak apa adanya.
const cek = `Teks mentah: {{.}}`
t3 := template.Must(template.New("cek").Parse(cek))
var sb strings.Builder
if err := t3.Execute(&sb, `<b>tebal</b> & "kutip"`); err != nil {
panic(err)
}
fmt.Println(sb.String())
}