# Design Pattern Go: Pattern Esensial dan Pertanyaan Wawancara untuk Developer Go > Kuasai design pattern Go: Functional Options, Strategy, Factory, dan Observer. Contoh kode praktis, praktik terbaik yang idiomatis, dan pertanyaan wawancara umum untuk developer Go. - Published: 2026-05-28 - Updated: 2026-06-06 - Author: SharpSkill - Tags: go, design-patterns, best-practices, interview, software-architecture - Reading time: 10 min --- Design pattern di Go berbeda secara mendasar dari padanannya pada bahasa berorientasi objek. Tanpa kelas maupun pewarisan, Go mengandalkan komposisi, interface, dan fungsi sebagai first-class citizen untuk mencapai fleksibilitas struktural yang sama, sering kali dengan lebih sedikit kerumitan dan lebih banyak kejelasan. > **Prinsip Idiomatis Go** > > Go mengutamakan komposisi daripada pewarisan. Setiap design pattern klasik perlu disesuaikan agar bekerja dengan interface, struct embedding, dan fungsi sebagai first-class citizen, bukan dengan hierarki kelas. ## Pattern Functional Options untuk konfigurasi yang fleksibel Pattern Functional Options menyelesaikan masalah umum di Go: membangun objek dengan banyak parameter opsional. Berbeda dengan bahasa yang memiliki method overloading atau argumen default, Go tidak menyediakan keduanya. Pattern Builder memang bisa, tetapi terasa bertele-tele. Functional Options menghadirkan API yang bersih dan mudah diperluas serta tetap kompatibel mundur seiring kebutuhan bertambah. Pattern ini, yang [dipopulerkan oleh Dave Cheney](https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis) dan kini menjadi standar di seluruh ekosistem Go, menggunakan argumen variadic untuk mengonfigurasi sebuah struct. ```go // server.go package server import ( "time" "log/slog" ) // Server holds the HTTP server configuration. type Server struct { host string port int timeout time.Duration maxConns int logger *slog.Logger } // Option defines a functional option for Server. type Option func(*Server) // WithPort sets the server port. func WithPort(port int) Option { return func(s *Server) { s.port = port } } // WithTimeout sets the request timeout. func WithTimeout(d time.Duration) Option { return func(s *Server) { s.timeout = d } } // WithMaxConns sets the maximum concurrent connections. func WithMaxConns(n int) Option { return func(s *Server) { s.maxConns = n } } // New creates a Server with sensible defaults and applies options. func New(host string, opts ...Option) *Server { srv := &Server{ host: host, port: 8080, // default port timeout: 30 * time.Second, // default timeout maxConns: 100, // default max connections logger: slog.Default(), } for _, opt := range opts { opt(srv) } return srv } ``` Pemanggil hanya menentukan apa yang berbeda dari nilai default. Menambahkan opsi baru tidak pernah merusak call site yang sudah ada. Pattern ini muncul di pustaka produksi seperti `google.golang.org/grpc` dan `go.uber.org/zap`. ## Pattern Strategy melalui pemenuhan interface Pattern Strategy membungkus algoritma yang dapat saling dipertukarkan di balik sebuah interface bersama. Di Go, hal ini langsung dipetakan ke polimorfisme berbasis interface, tanpa kelas abstrak atau rantai pewarisan. Sistem pemrosesan pembayaran menggambarkan pattern ini. Setiap metode pembayaran mengimplementasikan interface yang sama, dan layanan checkout memilih strategi pada saat runtime. ```go // payment.go package payment import "fmt" // Processor defines the strategy interface. type Processor interface { Pay(amount float64) (string, error) } // CreditCard implements Processor for card payments. type CreditCard struct { CardNumber string Expiry string } func (c *CreditCard) Pay(amount float64) (string, error) { // Charge the card via payment gateway return fmt.Sprintf("charged %.2f to card ending %s", amount, c.CardNumber[len(c.CardNumber)-4:]), nil } // BankTransfer implements Processor for wire transfers. type BankTransfer struct { IBAN string } func (b *BankTransfer) Pay(amount float64) (string, error) { return fmt.Sprintf("initiated transfer of %.2f to %s", amount, b.IBAN), nil } // Checkout processes a payment using the given strategy. func Checkout(p Processor, amount float64) error { receipt, err := p.Pay(amount) if err != nil { return fmt.Errorf("payment failed: %w", err) } fmt.Println(receipt) return nil } ``` Interface di Go dipenuhi secara implisit, tanpa kata kunci `implements`. Tipe apa pun yang memiliki method `Pay(float64) (string, error)` memenuhi syarat sebagai `Processor`. Hal ini menjaga coupling tetap rendah dan membuat pengujian menjadi sederhana: cukup berikan `Processor` tiruan yang mengembalikan hasil yang dapat diprediksi. ## Fungsi factory dan pattern konstruktor di Go Go tidak memiliki konstruktor. Penggantinya yang idiomatis adalah fungsi factory, biasanya bernama `New` atau `NewXxx`, yang mengembalikan struct yang sudah diinisialisasi. Fungsi factory menegakkan invariant yang tidak dapat dijamin oleh inisialisasi zero-value. Perbedaan ini penting untuk persiapan wawancara: pengembang Go perlu mengenali kapan fungsi factory diperlukan dibandingkan kapan [zero value berguna secara default](https://go.dev/blog/organizing-go-code). ```go // connpool.go package pool import ( "errors" "sync" ) // ConnPool manages a pool of reusable connections. type ConnPool struct { mu sync.Mutex conns []Conn maxSize int } // Conn represents a database connection. type Conn struct { ID int Active bool } // NewConnPool validates parameters and returns an initialized pool. func NewConnPool(maxSize int) (*ConnPool, error) { if maxSize <= 0 { return nil, errors.New("pool: maxSize must be positive") } return &ConnPool{ conns: make([]Conn, 0, maxSize), maxSize: maxSize, }, nil } // Acquire returns a connection from the pool. func (p *ConnPool) Acquire() (*Conn, error) { p.mu.Lock() defer p.mu.Unlock() for i := range p.conns { if !p.conns[i].Active { p.conns[i].Active = true return &p.conns[i], nil } } if len(p.conns) >= p.maxSize { return nil, errors.New("pool: no available connections") } c := Conn{ID: len(p.conns) + 1, Active: true} p.conns = append(p.conns, c) return &p.conns[len(p.conns)-1], nil } ``` Fungsi factory `NewConnPool` memvalidasi parameter `maxSize` dan mengalokasikan slice di awal. Inisialisasi struct secara langsung (`&ConnPool{}`) akan melewati validasi tersebut sehingga berpotensi menimbulkan panic saat runtime. Pattern ini meluas secara alami: gabungkan dengan Functional Options untuk skenario konfigurasi yang lebih kompleks. ## Pattern Observer dengan channel dan goroutine Pattern Observer memberi tahu banyak subscriber saat state berubah. Di Java atau C#, hal ini melibatkan event listener dan registrasi callback. Go menawarkan jalur yang lebih idiomatis: channel. Setiap subscriber menerima event melalui channel-nya sendiri, dan goroutine menangani pengiriman secara concurrent. Pendekatan ini langsung memanfaatkan [primitif konkurensi](/blog/go/go-concurrency-goroutines-channels) Go dan menghindari kekusutan callback. ```go // eventbus.go package events import "sync" // Event carries a topic and payload. type Event struct { Topic string Payload any } // Bus manages subscriptions and event dispatch. type Bus struct { mu sync.RWMutex subscribers map[string][]chan Event } // NewBus creates an event bus. func NewBus() *Bus { return &Bus{ subscribers: make(map[string][]chan Event), } } // Subscribe returns a channel that receives events for a topic. func (b *Bus) Subscribe(topic string) <-chan Event { ch := make(chan Event, 16) // buffered to avoid blocking publisher b.mu.Lock() b.subscribers[topic] = append(b.subscribers[topic], ch) b.mu.Unlock() return ch } // Publish sends an event to all subscribers of the topic. func (b *Bus) Publish(topic string, payload any) { b.mu.RLock() defer b.mu.RUnlock() for _, ch := range b.subscribers[topic] { select { case ch <- Event{Topic: topic, Payload: payload}: default: // subscriber too slow, drop event } } } ``` Penggunaan `select` dengan klausa `default` mencegah subscriber yang lambat memblokir seluruh bus. Pada sistem produksi, menambahkan mekanisme pembatalan berbasis context dan method `Unsubscribe` akan melengkapi implementasinya. ## Pattern Middleware untuk pipeline permintaan HTTP Rantai middleware adalah tulang punggung server HTTP di Go. Pattern ini membungkus sebuah `http.Handler` dengan perilaku tambahan (logging, autentikasi, rate limiting) tanpa mengubah handler itu sendiri. Interface `http.Handler` dari pustaka standar membuat komposisi ini sangat mudah. ```go // middleware.go package middleware import ( "log/slog" "net/http" "time" ) // Logging records request duration and status. func Logging(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() wrapped := &statusWriter{ResponseWriter: w, status: 200} next.ServeHTTP(wrapped, r) slog.Info("request", "method", r.Method, "path", r.URL.Path, "status", wrapped.status, "duration", time.Since(start), ) }) } // statusWriter captures the HTTP status code. type statusWriter struct { http.ResponseWriter status int } func (w *statusWriter) WriteHeader(code int) { w.status = code w.ResponseWriter.WriteHeader(code) } // Chain applies middleware in order: first listed = outermost. func Chain(h http.Handler, mw ...func(http.Handler) http.Handler) http.Handler { for i := len(mw) - 1; i >= 0; i-- { h = mw[i](h) } return h } ``` Fungsi `Chain` menerapkan middleware sesuai urutan deklarasi. Setiap middleware membungkus middleware berikutnya sehingga membentuk pipeline. Pattern ini identik dengan [router populer seperti chi](https://github.com/go-chi/chi), yang menerima `func(http.Handler) http.Handler` sebagai middleware. ## Komposisi daripada pewarisan dengan struct embedding Go tidak mendukung pewarisan. Sebagai gantinya, struct embedding mempromosikan method dari tipe dalam ke tipe luar sehingga mencapai penggunaan ulang kode tanpa coupling yang erat. Ini bukan pewarisan: tipe yang di-embed tidak mengetahui tipe yang meng-embed-nya, dan tidak ada virtual dispatch. Memahami perbedaan ini sangat penting untuk [pertanyaan wawancara Go](/technologies/go/interview-questions/concurrency-patterns). Kandidat yang mencampuradukkan embedding dengan pewarisan menunjukkan pemahaman yang dangkal tentang sistem tipe Go. ```go // models.go package models import "time" // Timestamps provides common audit fields. type Timestamps struct { CreatedAt time.Time UpdatedAt time.Time } // User embeds Timestamps to gain CreatedAt/UpdatedAt. type User struct { Timestamps ID int Email string } // Order also embeds Timestamps. type Order struct { Timestamps ID int UserID int Total float64 } ``` Kini baik `User` maupun `Order` mengekspos field `CreatedAt` dan `UpdatedAt` secara langsung. Method apa pun yang didefinisikan pada `Timestamps` juga ikut dipromosikan. Aturan utamanya: embedding menyediakan delegasi, bukan substitusi. Sebuah `User` bukanlah `Timestamps`; ia memiliki `Timestamps`. ## Pertanyaan wawancara tentang design pattern Go Pertanyaan design pattern dalam wawancara Go menguji apakah kandidat dapat menyesuaikan pattern klasik dengan sistem tipe Go. Pertanyaan berikut sering muncul di seleksi teknis dan sesi tatap muka. **T: Kapan fungsi factory sebaiknya mengembalikan error alih-alih melakukan panic?** Fungsi factory sebaiknya mengembalikan error setiap kali validasi bergantung pada input runtime (konfigurasi dari pengguna, variabel lingkungan, data eksternal). Panic disediakan untuk kesalahan pemrogram, yaitu situasi yang mengindikasikan bug, seperti meneruskan pointer nil padahal kontrak API melarangnya. Pustaka standar mengikuti konvensi ini: `os.Open` mengembalikan error, sedangkan `regexp.MustCompile` melakukan panic karena mengharapkan pola yang bersifat konstan pada saat kompilasi. **T: Bagaimana pattern Functional Options memperbaiki evolusi sebuah API?** Menambahkan fungsi `WithXxx` baru adalah perubahan yang tidak merusak. Pemanggil yang sudah ada tetap berfungsi tanpa modifikasi. Hal ini berbeda dengan struct konfigurasi, di mana menambahkan field wajib akan merusak semua call site, atau dengan parameter posisional, di mana menyusun ulang argumen memunculkan bug. **T: Apa yang membedakan interface Go dari interface Java atau C#?** Interface Go dipenuhi secara implisit. Sebuah tipe mengimplementasikan interface cukup dengan memiliki method yang dibutuhkan, tanpa perlu deklarasi `implements`. Hal ini memungkinkan prinsip "terima interface, kembalikan struct": mendefinisikan interface yang sempit di sisi pemanggil, bukan di sisi implementasi. Hasilnya adalah [coupling yang lebih rendah dan testability yang lebih tinggi](/technologies/go/interview-questions/testing). > **Pola Wawancara** > > Pewawancara sering meminta kandidat merefaktor dependensi konkret menjadi sebuah interface. Ujiannya: dapatkah kandidat mengidentifikasi himpunan method minimal yang dibutuhkan dan mengekstraknya di sisi konsumen, bukan di sisi produsen? **T: Bagaimana Observer berbasis channel berbeda dari Observer berbasis callback?** Channel memisahkan publisher dan subscriber baik dalam waktu maupun ruang. Publisher tidak menyimpan referensi ke fungsi subscriber; ia mengirim ke channel. Subscriber dapat memproses event sesuai temponya sendiri menggunakan channel ber-buffer. Pernyataan `select` memungkinkan penanganan timeout, pembatalan melalui `context`, dan multiplexing di antara beberapa sumber event, yang semuanya tidak otomatis tersedia pada callback. > **Jebakan Umum** > > Lupa menutup channel subscriber menyebabkan kebocoran goroutine. Setiap `Subscribe` harus memiliki `Unsubscribe` yang sesuai untuk menutup channel dan menghapusnya dari bus. ## Kesimpulan - Pattern Functional Options menggantikan builder dan struct konfigurasi dengan API yang bersih dan mudah diperluas serta tidak pernah merusak pemanggil yang sudah ada - Strategy di Go dipetakan ke polimorfisme berbasis interface: definisikan interface di sisi konsumen, bukan di sisi penyedia - Fungsi factory menegakkan invariant yang tidak dapat dijamin oleh inisialisasi zero-value; kembalikan error untuk input runtime, lakukan panic hanya untuk bug pemrogram - Observer berbasis channel memanfaatkan goroutine dan `select` untuk pengiriman event yang concurrent dan terpisah tanpa rantai callback - Rantai middleware HTTP disusun melalui signature `func(http.Handler) http.Handler`, yang identik antara pustaka standar dan router pihak ketiga - Struct embedding menyediakan delegasi, bukan pewarisan: memahami perbedaan ini memisahkan pengembang yang fasih Go dari mereka yang menerjemahkan pattern Java apa adanya - Keberhasilan wawancara menuntut peragaan adaptasi idiomatis pattern ke sistem tipe Go, bukan hafalan definisi Gang of Four ala buku teks --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/id/blog/go/go-design-patterns-essential-patterns-interview