Semua pola sebelumnya mengajarkan komponen hanya mengenal interface; pertanyaannya: siapa yang membangun implementasi dan menyambungkannya? Dependency injection menjawab dengan pembagian tugas yang tegas — komponen menerima dependensinya dari luar, dan satu tempat khusus bernama composition root merakit seluruh aplikasi. Go tidak punya framework DI bawaan, dan justru di situ letak kekuatannya: wiring manual hanyalah fungsi biasa yang mudah dibaca dan tidak butuh refleksi. Chapter ini merakit aplikasi registrasi lengkap dengan fake untuk skenario uji, lalu membandingkan pendekatan manual dengan google/wire dan uber/fx.

Constructor Injection: Dependensi Masuk lewat New #

Bentuk DI paling penting di Go sangat sederhana: dependensi dikirim sebagai parameter constructor.

go
func NewLayananRegistrasi(repo UserRepo, notifier Notifier) *LayananRegistrasi {
	return &LayananRegistrasi{repo: repo, notifier: notifier}
}

Signature constructor adalah dokumentasi yang tidak bisa bohong — siapa pun melihat layanan registrasi membutuhkan UserRepo dan Notifier tanpa membaca isi kodenya. Bila layanan membangun sendiri implementasinya, penukaran dan pengujian menjadi mustahil tanpa memodifikasi kode produksi. Constructor injection memberi dua hal: testability — parameter yang sama dapat diisi fake — dan explicitness — daftar dependensi terlihat di satu baris.

Composition Root: main Merakit Semua #

Aturan keduanya menentukan di mana tipe konkret boleh muncul: hanya di composition root. Fungsi main adalah composition root yang paling alami di Go — di sanalah MemoryUserRepo dan SMTPNotifier dibangun, disambungkan, dan disuntikkan ke layanan. Seluruh sisa program hanya melihat interface.

Composition root juga pemilik lifecycle: resource yang perlu ditutup mendapat defer Close() setelah dibangun, dan urutan penutupan dibalik dari urutan pembuatan. Chapter Repository Pattern telah menunjukkan sisi interface-nya; chapter ini menuntaskan sisi perakitannya.

Demo: Aplikasi Terwiring Penuh #

Berkas di-demo.go pada bagian bawah halaman merakit aplikasi registrasi: dua pengguna didaftarkan, pendaftaran ganda ditolak, lalu skenario uji mengganti SMTP dengan fake. Kode lengkapnya:

go
package main

import (
	"errors"
	"fmt"
)

// ErrEmailTerdaftar adalah error domain: pendaftaran ganda.
var ErrEmailTerdaftar = errors.New("email sudah terdaftar")

// UserRepo adalah kontrak penyimpanan pengguna.
type UserRepo interface {
	Save(nama, email string) error
	Exists(email string) bool
}

// Notifier adalah kontrak pengiriman notifikasi.
type Notifier interface {
	Send(email, pesan string) error
}

// LayananRegistrasi adalah komponen inti: hanya mengenal kontrak.
// Seluruh dependensi masuk lewat constructor NewLayananRegistrasi.
type LayananRegistrasi struct {
	repo     UserRepo
	notifier Notifier
}

func NewLayananRegistrasi(repo UserRepo, notifier Notifier) *LayananRegistrasi {
	return &LayananRegistrasi{repo: repo, notifier: notifier}
}

func (l *LayananRegistrasi) Daftar(nama, email string) error {
	if l.repo.Exists(email) {
		return fmt.Errorf("daftar %q: %w", email, ErrEmailTerdaftar)
	}
	if err := l.repo.Save(nama, email); err != nil {
		return err
	}
	return l.notifier.Send(email, "Halo "+nama+", selamat bergabung!")
}

// MemoryUserRepo adalah implementasi sungguhan.
// Di produksi isinya database; di sini cukup map.
type MemoryUserRepo struct {
	pengguna map[string]string // email -> nama
}

func NewMemoryUserRepo() *MemoryUserRepo {
	return &MemoryUserRepo{pengguna: map[string]string{}}
}

func (r *MemoryUserRepo) Save(nama, email string) error {
	r.pengguna[email] = nama
	return nil
}

func (r *MemoryUserRepo) Exists(email string) bool {
	_, ok := r.pengguna[email]
	return ok
}

// SMTPNotifier adalah implementasi notifikasi sungguhan.
type SMTPNotifier struct {
	Host string
}

func (n *SMTPNotifier) Send(email, pesan string) error {
	fmt.Printf("[smtp %s] kirim ke %s: %s\n", n.Host, email, pesan)
	return nil
}

// FakeNotifier menangkap pesan tanpa mengirim apa pun.
// Objek semacam ini dipakai sebagai pengganti saat pengujian.
type FakeNotifier struct {
	Terkirim []string
}

func (f *FakeNotifier) Send(email, pesan string) error {
	f.Terkirim = append(f.Terkirim, email+" <- "+pesan)
	return nil
}

func main() {
	// Composition root: satu-satunya tempat yang tahu cara
	// merakit seluruh komponen menjadi aplikasi utuh.
	repo := NewMemoryUserRepo()
	notifier := &SMTPNotifier{Host: "smtp.example.com"}
	app := NewLayananRegistrasi(repo, notifier)

	for _, p := range [][2]string{
		{"Budi", "[email protected]"},
		{"Siti", "[email protected]"},
		{"Budi Kedua", "[email protected]"},
	} {
		if err := app.Daftar(p[0], p[1]); err != nil {
			fmt.Println("gagal :", err)
			fmt.Println("errors.Is(ErrEmailTerdaftar) =", errors.Is(err, ErrEmailTerdaftar))
		}
	}

	// Skenario uji: SMTP diganti fake, kode layanan tidak berubah.
	fake := &FakeNotifier{}
	appUji := NewLayananRegistrasi(NewMemoryUserRepo(), fake)
	if err := appUji.Daftar("Siti", "[email protected]"); err != nil {
		fmt.Println("gagal :", err)
		return
	}
	fmt.Printf("fake menangkap %d pesan:\n", len(fake.Terkirim))
	for _, m := range fake.Terkirim {
		fmt.Println(" -", m)
	}
}

Output yang Dihasilkan:

text
[smtp smtp.example.com] kirim ke [email protected]: Halo Budi, selamat bergabung!
[smtp smtp.example.com] kirim ke [email protected]: Halo Siti, selamat bergabung!
gagal : daftar "[email protected]": email sudah terdaftar
errors.Is(ErrEmailTerdaftar) = true
fake menangkap 1 pesan:
 - [email protected] <- Halo Siti, selamat bergabung!

Mari kita telaah baris demi baris program di atas:

  1. Baris 12–21 (UserRepo dan Notifier): dua kontrak kecil, satu-satunya hal yang dikenal layanan.
  2. Baris 29–31 (NewLayananRegistrasi): constructor injection; dependensi masuk lewat parameter dan disimpan sebagai field.
  3. Baris 33–41 (method Daftar): alur bisnis murni — periksa, simpan, kirim; tidak ada satu pun konstruksi di dalamnya.
  4. Baris 45–72 (implementasi sungguhan): MemoryUserRepo menggantikan database, SMTPNotifier menggantikan server email asli.
  5. Baris 75–82 (FakeNotifier): test double yang menangkap pesan ke slice — bukti pengiriman dapat diperiksa tanpa jaringan.
  6. Baris 87–89 (wiring di main): composition root bekerja — repo dibangun, notifier dibangun, layanan dirakit dari keduanya.
  7. Baris 96–100 (email ganda): pendaftaran kedua dengan email yang sama gagal, dan errors.Is mengenali ErrEmailTerdaftar.
  8. Baris 103–104 (skenario uji): aplikasi kedua dirakit ulang dengan fake; kode layanan tidak berubah satu baris pun.

Manual, google/wire, dan uber/fx #

Wiring manual sempurna untuk aplikasi kecil hingga menengah. Ketika grafik komponen membesar — belasan constructor dengan dependensi bersarang — dua library populer turun tangan. google/wire bekerja lewat code generation: deklarasikan daftar provider, lalu go generate menuliskan kode wiring-nya.

go
// Fragmen konsep google/wire — bukan program yang dijalankan.
func initApp() *LayananRegistrasi {
	wire.Build(NewMemoryUserRepo, NewSMTPNotifier, NewLayananRegistrasi)
	return nil // diganti hasil generate berikutnya
}

Hasil wire adalah file Go biasa tanpa refleksi, sehingga kesalahan wiring tertangkap saat kompilasi. uber/fx memilih jalan sebaliknya: container runtime yang membaca dependency lewat refleksi, menyediakan hook lifecycle OnStart/OnStop, dan menjalankan aplikasi lewat fx.App. Perbandingan ketiganya:

AspekManualgoogle/wireuber/fx
Cara kerjatulis wiring sendiricodegen dari daftar providercontainer refleksi runtime
Biaya runtimenolnol — hasilnya kode manualada refleksi dan bootstrap
Kesalahan wiringtertangkap kompilasitertangkap saat generatebaru terlihat saat aplikasi start
Lifecyclekelola sendiri dengan defersama seperti manualframework mengurus start/stop
Cocok untukaplikasi kecil–menengahgrafik besar yang tetap ingin eksplisitaplikasi besar dengan banyak lifecycle

Anti-Pattern: Service Locator #

Variasi yang tampak menjanjikan namun berbahaya adalah service locator: registry global yang diisi dari mana saja, lalu tiap komponen mengambil sendiri dependensinya.

go
// ANTI-POLAR: registry global yang bisa diisi dari mana saja.
var Layanan = map[string]any{}

repo := Layanan["repo"].(UserRepo) // type assertion saat runtime

Signature constructor berhenti jujur: layanan tampak tidak butuh apa pun padahal diam-diam mengambil UserRepo dari registry. Type assertion bisa panic saat runtime, urutan pengisian menjadi kontrak tersembunyi, dan pengujian terpisah menjadi sulit karena seluruh test berbagi satu registry global. Constructor injection memindahkan semua risiko itu ke parameter yang diperiksa kompiler.

Aplikasi yang dirakit rapi memiliki mata rantai yang bisa diganti kapan pun: database sungguhan saat produksi, fake saat pengujian. Pola-pola pada section ini pada akhirnya bertemu di titik yang sama: komponen kecil yang hanya mengenal kontrak, dan satu composition root yang memegang seluruh keputusan.

Referensi #

  • Effective Go — komposisi dan constructor sebagai fondasi wiring manual.
  • Errors are values — penanganan error pada komponen yang disusun bertingkat.

Berkas Sumber

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

di-demo.go

Unduh berkas
go
package main

import (
	"errors"
	"fmt"
)

// ErrEmailTerdaftar adalah error domain: pendaftaran ganda.
var ErrEmailTerdaftar = errors.New("email sudah terdaftar")

// UserRepo adalah kontrak penyimpanan pengguna.
type UserRepo interface {
	Save(nama, email string) error
	Exists(email string) bool
}

// Notifier adalah kontrak pengiriman notifikasi.
type Notifier interface {
	Send(email, pesan string) error
}

// LayananRegistrasi adalah komponen inti: hanya mengenal kontrak.
// Seluruh dependensi masuk lewat constructor NewLayananRegistrasi.
type LayananRegistrasi struct {
	repo     UserRepo
	notifier Notifier
}

func NewLayananRegistrasi(repo UserRepo, notifier Notifier) *LayananRegistrasi {
	return &LayananRegistrasi{repo: repo, notifier: notifier}
}

func (l *LayananRegistrasi) Daftar(nama, email string) error {
	if l.repo.Exists(email) {
		return fmt.Errorf("daftar %q: %w", email, ErrEmailTerdaftar)
	}
	if err := l.repo.Save(nama, email); err != nil {
		return err
	}
	return l.notifier.Send(email, "Halo "+nama+", selamat bergabung!")
}

// MemoryUserRepo adalah implementasi sungguhan.
// Di produksi isinya database; di sini cukup map.
type MemoryUserRepo struct {
	pengguna map[string]string // email -> nama
}

func NewMemoryUserRepo() *MemoryUserRepo {
	return &MemoryUserRepo{pengguna: map[string]string{}}
}

func (r *MemoryUserRepo) Save(nama, email string) error {
	r.pengguna[email] = nama
	return nil
}

func (r *MemoryUserRepo) Exists(email string) bool {
	_, ok := r.pengguna[email]
	return ok
}

// SMTPNotifier adalah implementasi notifikasi sungguhan.
type SMTPNotifier struct {
	Host string
}

func (n *SMTPNotifier) Send(email, pesan string) error {
	fmt.Printf("[smtp %s] kirim ke %s: %s\n", n.Host, email, pesan)
	return nil
}

// FakeNotifier menangkap pesan tanpa mengirim apa pun.
// Objek semacam ini dipakai sebagai pengganti saat pengujian.
type FakeNotifier struct {
	Terkirim []string
}

func (f *FakeNotifier) Send(email, pesan string) error {
	f.Terkirim = append(f.Terkirim, email+" <- "+pesan)
	return nil
}

func main() {
	// Composition root: satu-satunya tempat yang tahu cara
	// merakit seluruh komponen menjadi aplikasi utuh.
	repo := NewMemoryUserRepo()
	notifier := &SMTPNotifier{Host: "smtp.example.com"}
	app := NewLayananRegistrasi(repo, notifier)

	for _, p := range [][2]string{
		{"Budi", "[email protected]"},
		{"Siti", "[email protected]"},
		{"Budi Kedua", "[email protected]"},
	} {
		if err := app.Daftar(p[0], p[1]); err != nil {
			fmt.Println("gagal :", err)
			fmt.Println("errors.Is(ErrEmailTerdaftar) =", errors.Is(err, ErrEmailTerdaftar))
		}
	}

	// Skenario uji: SMTP diganti fake, kode layanan tidak berubah.
	fake := &FakeNotifier{}
	appUji := NewLayananRegistrasi(NewMemoryUserRepo(), fake)
	if err := appUji.Daftar("Siti", "[email protected]"); err != nil {
		fmt.Println("gagal :", err)
		return
	}
	fmt.Printf("fake menangkap %d pesan:\n", len(fake.Terkirim))
	for _, m := range fake.Terkirim {
		fmt.Println(" -", m)
	}
}