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.

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

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

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

text
default  timeout=5s       maxConns=10
payment  timeout=2s       maxConns=10
report   timeout=500ms    maxConns=64

Mari kita telaah baris demi baris program di atas:

  1. Baris 10 (type Option func(*Server)): opsi adalah fungsi yang menerima pointer ke Server — struct yang sedang dibangun.
  2. Baris 13–17 (WithTimeout): fungsi pembuat opsi yang mengembalikan closure; nilai d terikat di closure dan ditulis ke s.timeout saat opsi diterapkan.
  3. Baris 26–31 (WithName): pola yang sama untuk bidang string; satu fungsi WithX hanya mengisi satu bidang.
  4. Baris 42 (func NewServer(opts ...Option)): parameter variadic menampung nol atau lebih opsi tanpa mengubah signature.
  5. Baris 43–47 (struct literal bawaan): seluruh nilai bawaan ditetapkan di satu tempat; pemanggil tanpa opsi tetap mendapat konfigurasi sah.
  6. Baris 48–50 (loop penerapan): tiap opsi dipanggil dengan s berurutan; opsi terakhir yang menyentuh bidang yang sama menang.
  7. 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:

AspekFunctional OptionsConfig Struct
Nilai bawaanditetapkan sekali di constructordiisi pemanggil atau helper terpisah
Opsi barucukup tambah WithX; pemanggil lama tak berubahfield baru membuat pemanggil lama diam-diam memakai nilai nol
Keterbacaan call siteWithTimeout(2*time.Second) menyebut maksudnyabergantung nama field dan komentar
Konflik pengaturandua opsi sama: opsi terakhir menang senyapfield hanya bisa diisi sekali
Biaya penulisansatu fungsi kecil per opsisatu struct, langsung selesai
Cocok untukAPI publik library yang terus tumbuhkode 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 #

Berkas Sumber

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

options-demo.go

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