Semakin besar sebuah library, semakin banyak pengaturan yang harus diterima konstruktornya: batas waktu, jumlah koneksi, hingga nama instance untuk log. Konstruktor dengan banyak parameter posisi memaksa pemanggil menghitung koma satu per satu, dan penambahan parameter memaksa pemanggil lama disesuaikan. Functional options menyelesaikan masalah tersebut dengan trik sederhana: setiap pengaturan dikemas sebagai fungsi kecil yang mengubah struct konfigurasi dari dalam. Pada chapter ini kita akan membangun pola itu dari nol, membandingkannya dengan config struct biasa, lalu melihat alasan pola ini menjadi idiom standar pada library Go besar seperti grpc-go.
Masalah Konstruktor yang Membengkak #
Bentuk paling sederhana dari masalah ini adalah konstruktor dengan parameter posisi. Bayangkan NewServer(name string, timeout time.Duration, maxConns int): tiga parameter masih terkendali, tetapi daftar itu jarang berhenti di tiga. Setiap fitur baru menambah satu parameter dan memaksa semua call site lama diketik ulang.
// Semua argumen posisi: mudah tertukar dan rapuh terhadap perubahan.
srv := NewServer("payment", 5*time.Second, 10)Parameter posisi juga rawan tertukar tanpa terdeteksi. Tipe time.Duration dan int sama-sama angka, sehingga kompiler tidak menolak pemanggilan yang menukar keduanya. Nilai nol seperti 0 untuk timeout menimbulkan pertanyaan tambahan: apakah maksudnya “tanpa batas” atau pemanggil lupa mengisi nilainya? Pemanggil yang hanya butuh satu pengaturan tetap dipaksa menyebut sisanya.
Opsi Berupa Fungsi #
Functional options membalik arah pengisian konfigurasi. Alih-alih mengirim nilai mentah, pemanggil mengirim fungsi yang tahu cara mengisi satu bidang struct. Deklarasi tipenya hanya satu baris:
type Option func(*Server)
func WithTimeout(d time.Duration) Option {
return func(s *Server) {
s.timeout = d
}
}Fungsi WithTimeout menerima time.Duration, lalu mengembalikan closure yang menyimpan nilai tersebut. Ketika constructor memanggil closure itu dengan pointer *Server, closure menulis d ke bidang s.timeout. Nama pengaturan kini terbaca seperti kalimat: WithTimeout(2*time.Second) menjelaskan dirinya sendiri.
Constructor memakai parameter variadic opts ...Option sehingga pemanggil boleh mengirim nol, satu, atau sepuluh opsi. Nilai bawaan ditetapkan satu kali di dalam constructor sebelum loop menerapkan tiap opsi, sehingga opsi hanya menimpa bidang yang benar-benar diminta pemanggil.
Server dengan Tiga Opsi #
Berkas options-demo.go pada bagian bawah halaman menggabungkan seluruh bagian pola ini: tipe Option, tiga fungsi WithX, constructor dengan nilai bawaan, serta tiga variasi pemanggilan. Kode lengkapnya adalah sebagai berikut:
package main
import (
"fmt"
"time"
)
// Option adalah fungsi yang mengubah konfigurasi Server.
// Satu Option hanya bertugas mengisi satu bidang konfigurasi.
type Option func(*Server)
// WithTimeout mengembalikan Option yang mengatur batas waktu operasi.
func WithTimeout(d time.Duration) Option {
return func(s *Server) {
s.timeout = d
}
}
// WithMaxConns mengembalikan Option yang mengatur jumlah koneksi maksimum.
func WithMaxConns(n int) Option {
return func(s *Server) {
s.maxConns = n
}
}
// WithName mengembalikan Option yang memberi nama server untuk keperluan log.
func WithName(name string) Option {
return func(s *Server) {
s.name = name
}
}
// Server menyimpan hasil penerapan seluruh opsi.
type Server struct {
name string
timeout time.Duration
maxConns int
}
// NewServer membuat Server dengan nilai bawaan, lalu menerapkan
// opsi satu per satu lewat parameter variadic opts.
func NewServer(opts ...Option) *Server {
s := &Server{
name: "default",
timeout: 5 * time.Second,
maxConns: 10,
}
for _, opt := range opts {
opt(s)
}
return s
}
func main() {
a := NewServer()
b := NewServer(WithName("payment"), WithTimeout(2*time.Second))
c := NewServer(
WithName("report"),
WithMaxConns(64),
WithTimeout(500*time.Millisecond),
)
for _, s := range []*Server{a, b, c} {
fmt.Printf("%-8s timeout=%-8s maxConns=%d\n", s.name, s.timeout, s.maxConns)
}
}Output yang Dihasilkan:
default timeout=5s maxConns=10
payment timeout=2s maxConns=10
report timeout=500ms maxConns=64Mari kita telaah baris demi baris program di atas:
- Baris 10 (
type Option func(*Server)): opsi adalah fungsi yang menerima pointer keServer— struct yang sedang dibangun. - Baris 13–17 (
WithTimeout): fungsi pembuat opsi yang mengembalikan closure; nilaidterikat di closure dan ditulis kes.timeoutsaat opsi diterapkan. - Baris 26–31 (
WithName): pola yang sama untuk bidang string; satu fungsiWithXhanya mengisi satu bidang. - Baris 42 (
func NewServer(opts ...Option)): parameter variadic menampung nol atau lebih opsi tanpa mengubah signature. - Baris 43–47 (struct literal bawaan): seluruh nilai bawaan ditetapkan di satu tempat; pemanggil tanpa opsi tetap mendapat konfigurasi sah.
- Baris 48–50 (loop penerapan): tiap opsi dipanggil dengan
sberurutan; opsi terakhir yang menyentuh bidang yang sama menang. - Baris 57–61 (pemanggilan berantai): tiga opsi dirangkai dalam satu pemanggilan; urutan argumen bebas karena tiap opsi menyebut bidangnya.
Functional Options vs Config Struct #
Alternatif terdekat dari pola ini adalah struct konfigurasi biasa yang dikirim sebagai satu argumen. Keduanya sah; pilihan bergantung pada posisi API tersebut dipakai:
| Aspek | Functional Options | Config Struct |
|---|---|---|
| Nilai bawaan | ditetapkan sekali di constructor | diisi pemanggil atau helper terpisah |
| Opsi baru | cukup tambah WithX; pemanggil lama tak berubah | field baru membuat pemanggil lama diam-diam memakai nilai nol |
| Keterbacaan call site | WithTimeout(2*time.Second) menyebut maksudnya | bergantung nama field dan komentar |
| Konflik pengaturan | dua opsi sama: opsi terakhir menang senyap | field hanya bisa diisi sekali |
| Biaya penulisan | satu fungsi kecil per opsi | satu struct, langsung selesai |
| Cocok untuk | API publik library yang terus tumbuh | kode internal dengan opsi stabil |
grpc-go memakai pola ini pada grpc.Dial lewat grpc.WithTransportCredentials dan puluhan opsi lain; banyak library mengikuti jejak yang sama. Kuncinya: signature constructor tidak pernah berubah, sehingga opsi baru tidak merusak pemanggil lama.
Opsi Ganda dan Validasi #
Kabar baiknya, kesalahan ketik terkunci sejak kompilasi: penulisan WithTimeOut ditolak kompiler karena fungsi tersebut tidak ada, berbeda dengan konfigurasi berbasis map atau JSON yang menerima kunci tak dikenal tanpa suara.
Functional options menjaga call site tetap pendek sekalipun jumlah pengaturan terus bertambah. Setelah konfigurasi, langkah berikutnya adalah menyembunyikan pemilihan tipe itu sendiri: chapter Factory Pattern akan menunjukkan cara membangun objek lewat fungsi pabrik tanpa memaksa pemanggil mengenal struct konkret di baliknya.
Referensi #
- Effective Go — fungsi sebagai nilai dan idiom penulisan API Go.
- Dokumentasi package fmt
— referensi
fmt.Printfyang dipakai demo untuk mencetak konfigurasi.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
options-demo.go
Unduh berkaspackage main
import (
"fmt"
"time"
)
// Option adalah fungsi yang mengubah konfigurasi Server.
// Satu Option hanya bertugas mengisi satu bidang konfigurasi.
type Option func(*Server)
// WithTimeout mengembalikan Option yang mengatur batas waktu operasi.
func WithTimeout(d time.Duration) Option {
return func(s *Server) {
s.timeout = d
}
}
// WithMaxConns mengembalikan Option yang mengatur jumlah koneksi maksimum.
func WithMaxConns(n int) Option {
return func(s *Server) {
s.maxConns = n
}
}
// WithName mengembalikan Option yang memberi nama server untuk keperluan log.
func WithName(name string) Option {
return func(s *Server) {
s.name = name
}
}
// Server menyimpan hasil penerapan seluruh opsi.
type Server struct {
name string
timeout time.Duration
maxConns int
}
// NewServer membuat Server dengan nilai bawaan, lalu menerapkan
// opsi satu per satu lewat parameter variadic opts.
func NewServer(opts ...Option) *Server {
s := &Server{
name: "default",
timeout: 5 * time.Second,
maxConns: 10,
}
for _, opt := range opts {
opt(s)
}
return s
}
func main() {
a := NewServer()
b := NewServer(WithName("payment"), WithTimeout(2*time.Second))
c := NewServer(
WithName("report"),
WithMaxConns(64),
WithTimeout(500*time.Millisecond),
)
for _, s := range []*Server{a, b, c} {
fmt.Printf("%-8s timeout=%-8s maxConns=%d\n", s.name, s.timeout, s.maxConns)
}
}