Template satu blok besar cepat menjadi beban: header dan footer yang sama diulang di setiap halaman, dan satu perubahan desain harus disalin ke mana-mana. Solusinya adalah komposisi — memecah template menjadi partial kecil, menyusun kerangka layout, lalu menimpa bagian tertentu untuk tiap halaman. Package text/template menyediakan tiga alat untuk itu: aksi {{define}} untuk menamai potongan, {{template}} untuk memanggilnya, dan {{block}} untuk slot yang punya isi default. Pada chapter ini kita akan menyusun ketiganya menjadi satu aplikasi kecil yang merender halaman beranda dan tentang dari satu layout bersama.
Partial dengan define dan template #
Aksi {{define "nama"}}...{{end}} memberi nama pada sepotong template. Definisi ini diekstrak saat proses parsing dan tidak mencetak apa pun pada posisinya. Aksi kebalikannya, {{template "nama" .}}, mengeksekusi potongan tersebut — argumen kedua . meneruskan dot saat ini sebagai dot di dalam partial, sehingga partial bisa membaca field data yang sama. Pola ini cocok untuk header, footer, dan komponen yang muncul di banyak halaman.
block: Slot dengan Konten Default #
Aksi {{block "content" .}}...{{end}} adalah gabungan dua aksi sekaligus: satu {{define "content"}} berisi teks default, diikuti {{template "content" .}} yang langsung memanggilnya. Hasilnya adalah slot berisi default yang bisa ditimpa: halaman yang tidak mendefinisikan ulang "content" mendapat teks default, sedangkan halaman yang mendefinisikan ulang akan menggantinya. Definisi yang datang belakangan selalu menang dalam satu set template — perilaku yang kita manfaatkan untuk pola layout.
Pola Layout dengan Clone #
Urutan kerja pola layout: buat template base berisi layout penuh dengan {{block}}, panggil Clone untuk menduplikasi seluruh set, lalu Parse definisi "content" milik halaman pada hasil klon tersebut. Karena Clone dan Parse masing-masing mengembalikan pasangan nilai-error, kode nyaman dibungkus template.Must bertingkat. Setiap halaman mendapat salinan layout sendiri, bebas menimpa slot tanpa mengganggu halaman lain.
Program Utuh: Layout Beranda dan Tentang #
Berkas composition-demo.go mendemonstrasikan tiga teknik secara berurutan: partial, layout dengan block, lalu pemuatan dari filesystem tiruan:
package main
import (
"bytes"
"fmt"
"testing/fstest"
"text/template"
)
// Halaman adalah data yang diteruskan ke seluruh template komposisi.
type Halaman struct {
Judul string
Nama string
}
// partials berisi dua template kecil yang dipakai ulang di banyak halaman.
// {{define}} menamai potongan; aksi ini tidak mencetak apa pun.
const partials = `{{define "header"}}== {{.Judul}} =={{end}}{{define "footer"}}-- copyright Toko Contoh --{{end}}`
// layoutSrc adalah kerangka halaman; block "content" memberi
// konten default yang dapat ditimpa halaman turunan.
const layoutSrc = `{{template "header" .}}
{{block "content" .}}(halaman tanpa konten){{end}}
{{template "footer" .}}`
// render mengeksekusi template utama dan mengembalikan hasilnya.
func render(t *template.Template, data Halaman) string {
var buf bytes.Buffer
if err := t.Execute(&buf, data); err != nil {
panic(err)
}
return buf.String()
}
// renderNama mengeksekusi template bernama dari sebuah set.
func renderNama(t *template.Template, nama string, data Halaman) string {
var buf bytes.Buffer
if err := t.ExecuteTemplate(&buf, nama, data); err != nil {
panic(err)
}
return buf.String()
}
func main() {
berandaData := Halaman{Judul: "Beranda", Nama: "Budi"}
tentangData := Halaman{Judul: "Tentang", Nama: "Budi"}
// Bagian 1: satu set template dari dua panggilan Parse.
const berandaSrc = `{{template "header" .}}
Selamat datang, {{.Nama}}.
{{template "footer" .}}`
set := template.Must(template.New("beranda").Parse(partials))
template.Must(set.Parse(berandaSrc))
fmt.Println("=== 1. define + template ===")
fmt.Println(render(set, berandaData))
// Bagian 2: layout dengan Clone lalu timpa block "content".
base := template.Must(template.New("layout").Parse(partials))
template.Must(base.Parse(layoutSrc))
berandaT := template.Must(template.Must(base.Clone()).Parse(
`{{define "content"}}Selamat datang, {{.Nama}}.{{end}}`))
tentangT := template.Must(template.Must(base.Clone()).Parse(
`{{define "content"}}Toko Contoh menjual buku dan merchandise Go.{{end}}`))
fmt.Println("=== 2. block: default lalu override ===")
fmt.Println("[base] " + render(base, berandaData))
fmt.Println("[beranda] " + render(berandaT, berandaData))
fmt.Println("[tentang] " + render(tentangT, tentangData))
// Bagian 3: ParseFS — setiap set dibangun dari base + satu halaman.
// fstest.MapFS meniru folder berkas template sungguhan sehingga
// contoh tetap deterministik tanpa membaca berkas dari disk.
fsys := fstest.MapFS{
"layout/base.tmpl": &fstest.MapFile{Data: []byte(partials + layoutSrc)},
"pages/beranda.tmpl": &fstest.MapFile{Data: []byte(`{{define "content"}}Selamat datang, {{.Nama}}.{{end}}`)},
"pages/tentang.tmpl": &fstest.MapFile{Data: []byte(`{{define "content"}}Toko Contoh menjual buku dan merchandise Go.{{end}}`)},
}
fmt.Println("=== 3. ParseFS dari berkas ===")
halaman := []struct {
berkas string
data Halaman
}{
{"pages/beranda.tmpl", berandaData},
{"pages/tentang.tmpl", tentangData},
}
for _, h := range halaman {
ts := template.Must(template.ParseFS(fsys, "layout/base.tmpl", h.berkas))
fmt.Printf("[base.tmpl + %s]\n%s\n", h.berkas, renderNama(ts, "base.tmpl", h.data))
}
}Output yang Dihasilkan:
=== 1. define + template ===
== Beranda ==
Selamat datang, Budi.
-- copyright Toko Contoh --
=== 2. block: default lalu override ===
[base] == Beranda ==
(halaman tanpa konten)
-- copyright Toko Contoh --
[beranda] == Beranda ==
Selamat datang, Budi.
-- copyright Toko Contoh --
[tentang] == Tentang ==
Toko Contoh menjual buku dan merchandise Go.
-- copyright Toko Contoh --
=== 3. ParseFS dari berkas ===
[base.tmpl + pages/beranda.tmpl]
== Beranda ==
Selamat datang, Budi.
-- copyright Toko Contoh --
[base.tmpl + pages/tentang.tmpl]
== Tentang ==
Toko Contoh menjual buku dan merchandise Go.
-- copyright Toko Contoh --Mari kita telaah baris demi baris program di atas:
- Baris 18 (
const partials): dua partial berisi{{define}}dibuat sebagai string satu baris; karena isidefinediekstrak saat parsing, tidak ada teks liar yang bocor ke output. - Baris 22-24 (
const layoutSrc): kerangka halaman memanggil header dan footer lewat{{template "header" .}}, sementara{{block "content" .}}menyediakan teks default(halaman tanpa konten). - Baris 53-54 (
Newlalu duaParse): dua panggilanParsepada satu objek template membentuk satu set asosiasi —partialsmenyumbang header dan footer, panggilan kedua mengisi badan templateberanda. - Baris 60-61 (pembuatan base): template
layoutdibangun dengan pola yang sama; hasil eksekusinya di bagian[base]pada output masih menampilkan konten default. - Baris 63-66 (
Clone+Parsetimpaan):template.Must(template.Must(base.Clone()).Parse(...))mengklon layout lalu mendefinisikan ulang"content"; hasil klonnya adalah set beranda dan set tentang yang masing-masing utuh. - Baris 76-80 (
fstest.MapFS): peta berkas meniru folderlayout/danpages/sungguhan; isilayout/base.tmpladalah penggabunganpartialsdanlayoutSrc— persis isi berkas layout di proyek nyata. - Baris 90-92 (loop halaman): untuk setiap halaman,
ParseFSmembangun set baru darilayout/base.tmplplus satu berkas halaman;ExecuteTemplatemengeksekusi template bernama"base.tmpl"— nama berasal dari nama dasar berkas.
Perhatikan bahwa [base], [beranda], [tentang], dan hasil ParseFS memakai header serta footer yang persis sama: satu sumber kebenaran untuk komponen yang diulang.
Memuat Template dari Berkas #
Dalam proyek nyata, template disimpan sebagai berkas .tmpl tersendiri agar desainer dapat menyuntingnya tanpa menyentuh kode Go. Tiga fungsi pemuatan tersedia: ParseFiles membaca daftar berkas eksplisit, ParseGlob membaca semua berkas yang cocok dengan pola seperti templates/*.tmpl, dan ParseFS melakukan hal serupa dari fs.FS — termasuk embed.FS yang menempelkan template ke dalam binary. Setiap template dinamai dari nama dasar berkasnya, misalnya layout/base.tmpl menjadi template bernama "base.tmpl".
Program memakai pola satu set per halaman — ParseFS(fsys, "layout/base.tmpl", h.berkas) — bukan memuat semua halaman sekaligus. Alasannya: karena definisi terakhir selalu menang, memuat pages/beranda.tmpl dan pages/tentang.tmpl dalam satu set akan saling menimpa definisi "content" mereka. Pemisahan set per halaman menjaga setiap halaman tetap memakai timpaannya sendiri.
Layout yang tersusun rapi masih menyisakan satu kebutuhan: format tampilan yang tidak tersedia di aksi bawaan, seperti format mata uang. Kebutuhan itu dijawab oleh fungsi kustom — topik chapter berikutnya.
Referensi #
- Actions pada text/template
— sintaks resmi
define,template, danblock. - Dokumentasi package text/template
— API
Clone,ParseFiles,ParseGlob, danParseFS. - Dokumentasi package html/template — komposisi template dengan escaping otomatis.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
composition-demo.go
Unduh berkaspackage main
import (
"bytes"
"fmt"
"testing/fstest"
"text/template"
)
// Halaman adalah data yang diteruskan ke seluruh template komposisi.
type Halaman struct {
Judul string
Nama string
}
// partials berisi dua template kecil yang dipakai ulang di banyak halaman.
// {{define}} menamai potongan; aksi ini tidak mencetak apa pun.
const partials = `{{define "header"}}== {{.Judul}} =={{end}}{{define "footer"}}-- copyright Toko Contoh --{{end}}`
// layoutSrc adalah kerangka halaman; block "content" memberi
// konten default yang dapat ditimpa halaman turunan.
const layoutSrc = `{{template "header" .}}
{{block "content" .}}(halaman tanpa konten){{end}}
{{template "footer" .}}`
// render mengeksekusi template utama dan mengembalikan hasilnya.
func render(t *template.Template, data Halaman) string {
var buf bytes.Buffer
if err := t.Execute(&buf, data); err != nil {
panic(err)
}
return buf.String()
}
// renderNama mengeksekusi template bernama dari sebuah set.
func renderNama(t *template.Template, nama string, data Halaman) string {
var buf bytes.Buffer
if err := t.ExecuteTemplate(&buf, nama, data); err != nil {
panic(err)
}
return buf.String()
}
func main() {
berandaData := Halaman{Judul: "Beranda", Nama: "Budi"}
tentangData := Halaman{Judul: "Tentang", Nama: "Budi"}
// Bagian 1: satu set template dari dua panggilan Parse.
const berandaSrc = `{{template "header" .}}
Selamat datang, {{.Nama}}.
{{template "footer" .}}`
set := template.Must(template.New("beranda").Parse(partials))
template.Must(set.Parse(berandaSrc))
fmt.Println("=== 1. define + template ===")
fmt.Println(render(set, berandaData))
// Bagian 2: layout dengan Clone lalu timpa block "content".
base := template.Must(template.New("layout").Parse(partials))
template.Must(base.Parse(layoutSrc))
berandaT := template.Must(template.Must(base.Clone()).Parse(
`{{define "content"}}Selamat datang, {{.Nama}}.{{end}}`))
tentangT := template.Must(template.Must(base.Clone()).Parse(
`{{define "content"}}Toko Contoh menjual buku dan merchandise Go.{{end}}`))
fmt.Println("=== 2. block: default lalu override ===")
fmt.Println("[base] " + render(base, berandaData))
fmt.Println("[beranda] " + render(berandaT, berandaData))
fmt.Println("[tentang] " + render(tentangT, tentangData))
// Bagian 3: ParseFS — setiap set dibangun dari base + satu halaman.
// fstest.MapFS meniru folder berkas template sungguhan sehingga
// contoh tetap deterministik tanpa membaca berkas dari disk.
fsys := fstest.MapFS{
"layout/base.tmpl": &fstest.MapFile{Data: []byte(partials + layoutSrc)},
"pages/beranda.tmpl": &fstest.MapFile{Data: []byte(`{{define "content"}}Selamat datang, {{.Nama}}.{{end}}`)},
"pages/tentang.tmpl": &fstest.MapFile{Data: []byte(`{{define "content"}}Toko Contoh menjual buku dan merchandise Go.{{end}}`)},
}
fmt.Println("=== 3. ParseFS dari berkas ===")
halaman := []struct {
berkas string
data Halaman
}{
{"pages/beranda.tmpl", berandaData},
{"pages/tentang.tmpl", tentangData},
}
for _, h := range halaman {
ts := template.Must(template.ParseFS(fsys, "layout/base.tmpl", h.berkas))
fmt.Printf("[base.tmpl + %s]\n%s\n", h.berkas, renderNama(ts, "base.tmpl", h.data))
}
}