Aplikasi web hampir selalu perlu mengubah data menjadi HTML: daftar produk, profil pengguna, sampai halaman artikel. Kalau kita menggabungkan markup dengan operator + atau fmt.Sprintf, kode cepat dipenuhi penggabungan string yang sulit dibaca dan rentan celah keamanan. Package html/template dari standard library menyelesaikan kedua masalah tersebut sekaligus: satu berkas template yang deklaratif, plus proses escaping yang berjalan otomatis. Pada chapter ini kita akan membuat template pertama, mengeksekusinya ke bytes.Buffer, memakai aksi dasar range dan if, lalu mengamati sendiri bagaimana input berbahaya diubah menjadi teks yang aman.
Template String dan Aksi #
Sebuah template adalah teks biasa yang diselingi aksi di dalam tanda kurung ganda kurawal {{ }}. Teks di luar aksi dicetak apa adanya; aksi berisi instruksi yang dievaluasi terhadap data yang kita kirim. Aksi paling dasar adalah {{.Judul}}, yang membaca field Judul dari data. Tanda titik (.) merepresentasikan data aktif pada posisi tersebut — sering disebut dot.
| Aksi | Peran |
|---|---|
{{.Field}} | Membaca field struct atau nilai kunci map dari dot |
{{.}} | Mencetak dot itu sendiri |
{{range .Items}}...{{end}} | Mengulang setiap elemen slice; dot terikat ke elemen |
{{if .Stok}}...{{else}}...{{end}} | Memilih cabang berdasarkan nilai kebenaran data |
{{.Harga | printf "Rp%d"}} | Pipeline: hasil kiri diteruskan sebagai argumen terakhir fungsi kanan |
Aturan kebenaran if mengikuti konvensi Go: nilai kosong (false, 0, string "", slice nil) dianggap salah. Perilaku ini selaras dengan pembahasan chapter Kendali Alur
.
Membuat Template dengan template.Must #
Pembuatan template selalu memakai tiga langkah: template.New membuat objek template kosong dengan nama, Parse mengurai string template dan mengembalikan error bila sintaksnya salah, dan Execute menggabungkan template dengan data menjadi output. Karena template biasanya didefinisikan saat program dimulai, kesalahan Parse sebaiknya langsung menghentikan program — itulah peran template.Must:
t := template.Must(template.New("daftar").Parse(src))template.Must menerima pasangan nilai dan error; bila error tidak nil, fungsi tersebut memanggil panic. Pola ini menggantikan blok if err != nil untuk kondisi yang memang fatal sejak awal.
Program Utuh: Katalog Toko #
Berkas html-demo.go menggabungkan seluruh konsep di atas. Template merender katalog dua produk dengan aksi range, if, pipeline printf, lalu menampilkan komentar pembeli yang sengaja berisi tag <script> berbahaya:
package main
import (
"bytes"
"fmt"
"html"
"html/template"
)
// Produk mendeskripsikan satu item pada katalog toko.
type Produk struct {
Nama string
Harga int
Tags []string
Stok bool
}
// Katalog adalah data yang diteruskan ke template utama.
type Katalog struct {
Judul string
Items []Produk
Komentar string
Footer template.HTML
}
func main() {
const daftar = `<h1>{{.Judul}}</h1>
<ul>
{{range .Items}}<li>{{.Nama}} — {{.Harga | printf "Rp%d"}}{{if .Stok}} (stok tersedia){{else}} (stok habis){{end}}
{{range .Tags}} <span class="tag">{{.}}</span>
{{end}}</li>
{{else}}<li>Katalog masih kosong.</li>{{end}}
</ul>
<p>Komentar pembeli: {{.Komentar}}</p>
<p>{{.Footer}}</p>`
// template.Must memanggil panic bila Parse gagal: kesalahan
// sintaks template sebaiknya menghentikan program sejak awal.
t := template.Must(template.New("daftar").Parse(daftar))
data := Katalog{
Judul: "Katalog Toko Contoh",
Items: []Produk{
{Nama: "Buku Go", Harga: 95000, Tags: []string{"buku", "pemrograman"}, Stok: true},
{Nama: "Mug Keramik", Harga: 45000, Stok: false},
},
Komentar: `<script>alert("xss")</script>`,
// template.HTML menandai markup yang sudah tepercaya;
// bagian ini TIDAK di-escape saat dirender.
Footer: template.HTML(`<em>dirender oleh html/template</em>`),
}
var buf bytes.Buffer
if err := t.Execute(&buf, data); err != nil {
panic(err)
}
fmt.Print(buf.String())
fmt.Println()
// Data berupa map juga sah; akses field memakai nama kunci.
const sapaan = `<p>Selamat datang, <b>{{.nama}}</b> — level {{.level}}.</p>
`
t2 := template.Must(template.New("sapaan").Parse(sapaan))
dataMap := map[string]string{
"nama": "Siti & Budi",
"level": "gold",
}
var buf2 bytes.Buffer
if err := t2.Execute(&buf2, dataMap); err != nil {
panic(err)
}
fmt.Print(buf2.String())
fmt.Println("--- html.EscapeString pada string yang sama ---")
fmt.Println(html.EscapeString(`<script>alert("xss")</script>`))
}Output yang Dihasilkan:
<h1>Katalog Toko Contoh</h1>
<ul>
<li>Buku Go — Rp95000 (stok tersedia)
<span class="tag">buku</span>
<span class="tag">pemrograman</span>
</li>
<li>Mug Keramik — Rp45000 (stok habis)
</li>
</ul>
<p>Komentar pembeli: <script>alert("xss")</script></p>
<p><em>dirender oleh html/template</em></p>
<p>Selamat datang, <b>Siti & Budi</b> — level gold.</p>
--- html.EscapeString pada string yang sama ---
<script>alert("xss")</script>Mari kita telaah baris demi baris program di atas:
- Baris 19-24 (
type Katalog struct): mendefinisikan struct data; fieldFooterbertipetemplate.HTML, penanda bahwa isinya tepercaya dan boleh dirender tanpa escaping. - Baris 29 (
{{range .Items}}...): aksirangemengulang fieldItems; di dalam badan loop, dot terikat pada elemenProduksehingga{{.Nama}}dan{{.Stok}}merujuk field produk. - Baris 29 (
{{.Harga | printf "Rp%d"}}): pipeline mengirim nilaiHargasebagai argumen terakhir fungsiprintf, hasilnya sama denganprintf("Rp%d", harga). - Baris 30 (
{{range .Tags}}...{{.}}...):rangekedua mengulang sliceTags;{{.}}mencetak dot yang saat itu terikat pada elemen tag"buku"atau"pemrograman". - Baris 32 (
{{else}}<li>Katalog masih kosong.): cabangelsemilikrangedijalankan ketika sliceItemskosong —rangepunyaelse, sama sepertiif. - Baris 39 (
template.Must(...Parse(daftar))): mengurai template dan memanggil panic bila sintaks salah — program gagal cepat sebelum melayani permintaan. - Baris 53-58 (
bufdanExecute):Executemenulis hasil render kebytes.Buffer; error runtime (misalnya field tidak ada) baru bisa muncul saat eksekusi sehingga tetap diperiksa eksplisit. - Baris 63-73 (template kedua): template
sapaandieksekusi dengan datamap[string]string, membuktikan map dapat menggantikan struct sebagai sumber data.
Perhatikan baris <p>Komentar pembeli: <script>alert("xss")</script></p> pada output: tag <script> dan tanda kutip diubah menjadi entitas HTML sebelum dicetak. Browser akan menampilkannya sebagai teks biasa, bukan menjalankannya sebagai kode JavaScript.
Data Struct vs Map #
Kedua template pada program memakai sumber data yang berbeda. Struct memberikan kejelasan tipe — salah mengetik {{.Jdul}} menghasilkan error eksekusi yang mudah dilacak. Map lebih praktis untuk data yang bentuknya berubah-ubah, misalnya hasil query. Untuk penamaan: field struct harus diekspor (huruf awal kapital), sedangkan kunci map boleh huruf kecil seperti {{.nama}} karena kunci map tidak terikat aturan ekspor.
Perbedaan dengan html.EscapeString #
Bagian akhir program membandingkan hasil template dengan html.EscapeString pada string yang sama. Untuk konteks teks HTML biasa hasilnya memang identik — keduanya mengubah <, >, &, ', dan " menjadi entitas. Perbedaannya baru terasa pada posisi lain: html.EscapeString selalu melakukan satu transformasi yang sama, sedangkan html/template memeriksa posisi setiap aksi — di dalam atribut, JavaScript, CSS, atau URL — lalu memilih aturan escaping yang sesuai. Perilaku konteks-spesifik ini kita bedah tuntas pada chapter Escaping dan Keamanan Template
.
Dengan dasar ini, kita sudah bisa merender HTML yang aman dari data struct maupun map. Kadang target output kita bukan HTML melainkan teks biasa — laporan, faktur, atau badan email — dan untuk itu Go menyediakan paket kembar bernama text/template yang menjadi fokus chapter berikutnya.
Referensi #
- Dokumentasi package html/template — referensi resmi API, perilaku escaping, dan contoh pemakaian.
- Actions pada text/template — daftar lengkap aksi template yang berlaku juga untuk html/template.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
html-demo.go
Unduh berkaspackage main
import (
"bytes"
"fmt"
"html"
"html/template"
)
// Produk mendeskripsikan satu item pada katalog toko.
type Produk struct {
Nama string
Harga int
Tags []string
Stok bool
}
// Katalog adalah data yang diteruskan ke template utama.
type Katalog struct {
Judul string
Items []Produk
Komentar string
Footer template.HTML
}
func main() {
const daftar = `<h1>{{.Judul}}</h1>
<ul>
{{range .Items}}<li>{{.Nama}} — {{.Harga | printf "Rp%d"}}{{if .Stok}} (stok tersedia){{else}} (stok habis){{end}}
{{range .Tags}} <span class="tag">{{.}}</span>
{{end}}</li>
{{else}}<li>Katalog masih kosong.</li>{{end}}
</ul>
<p>Komentar pembeli: {{.Komentar}}</p>
<p>{{.Footer}}</p>`
// template.Must memanggil panic bila Parse gagal: kesalahan
// sintaks template sebaiknya menghentikan program sejak awal.
t := template.Must(template.New("daftar").Parse(daftar))
data := Katalog{
Judul: "Katalog Toko Contoh",
Items: []Produk{
{Nama: "Buku Go", Harga: 95000, Tags: []string{"buku", "pemrograman"}, Stok: true},
{Nama: "Mug Keramik", Harga: 45000, Stok: false},
},
Komentar: `<script>alert("xss")</script>`,
// template.HTML menandai markup yang sudah tepercaya;
// bagian ini TIDAK di-escape saat dirender.
Footer: template.HTML(`<em>dirender oleh html/template</em>`),
}
var buf bytes.Buffer
if err := t.Execute(&buf, data); err != nil {
panic(err)
}
fmt.Print(buf.String())
fmt.Println()
// Data berupa map juga sah; akses field memakai nama kunci.
const sapaan = `<p>Selamat datang, <b>{{.nama}}</b> — level {{.level}}.</p>
`
t2 := template.Must(template.New("sapaan").Parse(sapaan))
dataMap := map[string]string{
"nama": "Siti & Budi",
"level": "gold",
}
var buf2 bytes.Buffer
if err := t2.Execute(&buf2, dataMap); err != nil {
panic(err)
}
fmt.Print(buf2.String())
fmt.Println("--- html.EscapeString pada string yang sama ---")
fmt.Println(html.EscapeString(`<script>alert("xss")</script>`))
}