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.

Ilustrasi design pattern Go dengan bentuk geometris abstrak yang merepresentasikan arsitektur perangkat lunak

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 dan kini menjadi standar di seluruh ekosistem Go, menggunakan argumen variadic untuk mengonfigurasi sebuah struct.

server.gogo
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.

payment.gogo
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.

connpool.gogo
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.

Siap menguasai wawancara Go Anda?

Berlatih dengan simulator interaktif, flashcards, dan tes teknis kami.

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 Go dan menghindari kekusutan callback.

eventbus.gogo
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.

middleware.gogo
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, 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. Kandidat yang mencampuradukkan embedding dengan pewarisan menunjukkan pemahaman yang dangkal tentang sistem tipe Go.

models.gogo
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.

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.

Siap menguasai wawancara Go Anda?

Berlatih dengan simulator interaktif, flashcards, dan tes teknis kami.

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

Mulai berlatih!

Uji pengetahuan Anda dengan simulator wawancara dan tes teknis kami.

Tag

#go
#design-patterns
#best-practices
#interview
#software-architecture

Bagikan

Artikel terkait