← Semua pembelajaran / Go Nol → Enterprise
Fase 3 · HTTP — Request sampai Response

html/template — HTML yang aman secara default

html/template tahu apakah nilaimu sedang ditempatkan di dalam teks HTML, atribut, URL, atau JavaScript — dan mengubah cara escaping-nya sesuai konteks. Itu perbedaan besar dari text/template.

Sumber asli pkg.go.dev Resmi Rangkuman ~7 menit baca

Intisari

  • Selalu html/template, jangan pernah text/template untuk keluaran HTML.
  • Escaping-nya sadar konteks: nilai yang sama di-escape berbeda di dalam teks, atribut, URL, dan blok script.
  • Parse template sekali saat startup; template.Must membuat kesalahan template jadi kegagalan startup, bukan 500 di produksi.
  • embed.FS memasukkan template ke dalam binari — satu berkas untuk di-deploy.
  • Tipe template.HTML mematikan escaping. Anggap setiap pemakaiannya sebagai temuan review keamanan.

Escaping yang sadar konteks

data := map[string]string{
	"input": `<script>alert(1)</script>`,
}
{{.input}}                    →  &lt;script&gt;alert(1)&lt;/script&gt;
<a title="{{.input}}">        →  di-escape sebagai atribut
<a href="/cari?q={{.input}}"> →  di-escape sebagai URL
<script>var x = {{.input}};   →  di-escape sebagai literal string JavaScript
<style>color: {{.input}}      →  ditolak / dinetralkan sebagai CSS

Satu nilai, lima perlakuan berbeda — otomatis, berdasarkan tempat ia muncul. Inilah alasan XSS jauh lebih jarang di aplikasi Go yang memakai html/template dibandingkan aplikasi yang menyambung string HTML sendiri.

text/template punya API yang identik dan tidak melakukan escaping apa pun. Menukar satu huruf di baris import mengubah aplikasi aman jadi aplikasi rentan, tanpa satu pun error kompilasi. Periksa import ini di setiap review.

Parse sekali, di startup

import "embed"

//go:embed web/template/*.html
var templateFS embed.FS

// template.Must: kalau template rusak, program GAGAL START —
// bukan mengembalikan 500 pertama kali halaman itu dibuka.
var tmpl = template.Must(template.New("").Funcs(fungsi).
	ParseFS(templateFS, "web/template/*.html"))

func (h *Handler) Beranda(w http.ResponseWriter, r *http.Request) {
	data := halamanData{Judul: "Beranda", Produk: daftar}

	// Render ke buffer DULU: kalau gagal di tengah, kita masih bisa
	// mengirim 500 alih-alih halaman setengah jadi berstatus 200.
	var buf bytes.Buffer
	if err := tmpl.ExecuteTemplate(&buf, "beranda.html", data); err != nil {
		h.log.ErrorContext(r.Context(), "render gagal", "err", err)
		http.Error(w, "terjadi kesalahan", http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	w.Header().Set("Cache-Control", "public, s-maxage=60, stale-while-revalidate=600")
	_, _ = buf.WriteTo(w)
}

Dua keputusan di potongan itu yang layak ditiru: parse saat startup (bukan per permintaan — itu membaca disk dan memakai CPU di jalur panas), dan render ke buffer sebelum menulis. Yang kedua menghilangkan kelas bug "halaman terpotong dengan status 200" yang mustahil didiagnosis dari log.

Sintaks yang dipakai sehari-hari

{{.Judul}}                        nilai
{{.Produk.Nama}}                  bersarang
{{if .Login}}...{{else}}...{{end}}
{{range .Produk}} {{.Nama}} {{else}} Kosong {{end}}
{{with .Pengguna}} {{.Nama}} {{end}}     ganti konteks kalau tidak nil
{{template "kaki" .}}             sisipkan template lain
{{block "isi" .}}...{{end}}       definisi + pemanggilan sekaligus
{{$n := len .Produk}}             variabel
{{.Harga | rupiah}}               pipeline ke fungsi kustom
{{/* komentar, tidak ikut keluaran */}}
{{- .Nama -}}                     pangkas spasi kiri/kanan

Tata letak dengan blok

<!-- layout.html -->
<!doctype html>
<html lang="id">
<head><title>{{block "judul" .}}Toko{{end}}</title></head>
<body>
  {{block "isi" .}}{{end}}
</body>
</html>

<!-- beranda.html -->
{{define "judul"}}Beranda &middot; Toko{{end}}
{{define "isi"}}
  <h1>Produk</h1>
  <ul>{{range .Produk}}<li>{{.Nama}} — {{.Harga | rupiah}}</li>{{end}}</ul>
{{end}}

Fungsi kustom

var fungsi = template.FuncMap{
	"rupiah": func(n int64) string {
		return "Rp" + tandaRibuan(n)
	},
	"tanggal": func(t time.Time) string {
		return t.Format("2 Jan 2006")
	},
	"potong": func(s string, n int) string {
		if len(s) <= n {
			return s
		}
		return s[:n] + "…"
	},
}

Fungsi template harus murni dan cepat. Ia berjalan di tengah penulisan respons, tidak menerima context, dan tidak bisa mengembalikan error dengan rapi. Jangan pernah melakukan kueri database dari dalam template — siapkan semua data di handler.

template.HTML: satu-satunya jalan keluar, dan bahayanya

// Ini MEMATIKAN escaping. Kalau isinya pernah disentuh pengguna, ini XSS.
type artikel struct {
	Isi template.HTML
}

// Yang benar: bersihkan dulu dengan sanitizer, baru tandai aman.
isiBersih := bluemonday.UGCPolicy().Sanitize(markdownKeHTML(mentah))
a := artikel{Isi: template.HTML(isiBersih)}

Perlakukan setiap template.HTML, template.JS, dan template.URL sebagai temuan review. Semuanya berarti "aku menjamin ini aman" — dan jaminan itu harus punya alasan yang tertulis di sebelahnya. Sebagian besar XSS di aplikasi Go masuk lewat ketiga tipe ini.

Kapan template server-side masih pilihan terbaik

HalamanPendekatanKenapa
Katalog, artikel, landinghtml/templateBisa di-cache penuh di CDN; SEO bawaan; tanpa JavaScript
Form, filter, interaksi ringanTemplate + htmxInteraktif tanpa membangun SPA (Fase 7)
Panel admin kompleksSPA + API JSONDipakai sedikit orang; tidak perlu di-cache
Template dengan pemeriksaan tipetemplKesalahan template jadi error kompilasi (Fase 7)

Untuk situs bertrafik baca tinggi — yang jadi sasaran roadmap ini — halaman yang dirender server dan di-cache di CloudFront adalah kombinasi tersulit dikalahkan: permintaan yang tidak pernah menyentuh proses Go sama sekali.

Latihan: render halaman berisi {{.Komentar}} dengan nilai <img src=x onerror=alert(1)> dan lihat keluarannya aman. Lalu ubah tipe field jadi template.HTML dan muat ulang — perhatikan skripnya jalan. Terakhir, tukar import jadi text/template dan lihat semuanya bocor meski tipenya string.

Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.