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:

Aspektext/templatehtml/template
Sintaks aksiIdentikIdentik
Escaping otomatisTidak adaSesuai konteks HTML
Target khasLaporan, email, berkas teks, kode yang dihasilkanHalaman dan fragmen HTML
Tipe penanda tepercayaTidak adatemplate.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:

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

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

  1. Baris 18-20 (func (it Item) Subtotal): mendeklarasikan method pada struct Item; template dapat memanggil method tersebut seperti field, cukup dengan {{.Subtotal}} tanpa tanda kurung.
  2. Baris 30-36 (func (f Faktur) Total): method Total dijumlahkan dari seluruh subtotal; aksi {{.Total}} memanggilnya sekali setiap kali nilai dibutuhkan.
  3. Baris 44-46 ({{- range .Items}} … {{- end}}): trim marker memangkas baris kosong di sekitar badan loop sehingga baris item langsung menempel pada garis pemisah.
  4. Baris 45 ({{printf "%-18s ..." .Nama .Jumlah .Harga .Subtotal}}): aksi printf menerima empat argumen — method .Subtotal ikut menjadi argumen biasa, bukti method dan field setara di mata template.
  5. Baris 61-66 (parse dan execute): template diurai dengan template.Must lalu dieksekusi ke bytes.Buffer; hasil akhirnya dicetak sekali melalui fmt.Println.
  6. 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 dari html/template.
  7. Baris 76-78 (loop pada email): daftar item tampil sebagai butir - Nama sejumlah N pcs dengan 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 &lt;b&gt;tebal&lt;/b&gt; 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 #

Berkas Sumber

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

text-demo.go

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