Kodeloka
  1. Kodeloka
  2. Kelas Golang
  3. Materi

Domain 14 · 8 bab

Golang Dependency Injection

158. Konsep Dependency Injection & Loose Coupling

Memahami pentingnya pemisahan dependensi (Inversion of Control) untuk kode yang modular dan testable.

Apa itu Dependency Injection (DI)?

Dependency Injection adalah teknik desain perangkat lunak di mana sebuah objek (atau struct) menerima objek lain yang dibutuhkannya (dependensinya) dari luar, bukannya membuat sendiri dependensi tersebut di dalam constructor-nya.

💡 Masalah Keterikatan Erat (Tightly Coupled):
// TANPA DI (Tightly Coupled - Buruk):
type UserService struct {
    repo *MySQLUserRepository // Terikat mati ke MySQL! Susah di-unit test
}
func NewUserService() *UserService {
    return &UserService{repo: &MySQLUserRepository{}}
}
📌 DENGAN DI (Loosely Coupled - Ideal):
type UserService struct {
    repo UserRepository // Bergantung pada Interface!
}
func NewUserService(repo UserRepository) *UserService {
    return &UserService{repo: repo}
}

Contoh program

package main

import (
	"fmt"
)

type Repository interface {
	GetData() string
}

type MemoryRepo struct{}

func (r *MemoryRepo) GetData() string { return "Data dari Memory DB" }

type Service struct {
	repo Repository // Injeksi via Interface
}

func NewService(r Repository) *Service {
	return &Service{repo: r}
}

func main() {
	repo := &MemoryRepo{}
	svc := NewService(repo) // Dependency Injection Manual
	fmt.Println("Service Result:", svc.repo.GetData())
}

Praktik dan kuis bab ini di kelas interaktif →

159. Pengenalan Google Wire (Compile-Time DI Generator)

Mengenal Google Wire sebagai tool generator kode Dependency Injection tanpa reflection saat runtime.

Mengapa Google Wire?

Di bahasa pemrograman seperti Java (Spring) atau C# (.NET), Dependency Injection biasa menggunakan reflection saat aplikasi dinyalakan. Namun pada Go, reflection dapat memperlambat waktu start-up dan menimbulkan error di runtime.

💡 Karakteristik Google Wire:
  • Compile-Time Code Generation: Wire membuat kode Go standar (`wire_gen.go`) saat kompilasi.
  • Tanpa Reflection: Sangat cepat dan zero overhead saat aplikasi dijalankan.
  • Type-Safe: Jika ada dependensi yang kurang atau tipe data tidak cocok, kesalahan langsung terdeteksi saat wire di-run!

Instalasi CLI Wire:

go install github.com/google/wire/cmd/wire@latest

Contoh program

package main

import (
	"fmt"
)

func main() {
	fmt.Println("=== DEMO GOOGLE WIRE ===")
	fmt.Println("Google Wire adalah Compile-Time Dependency Injection tool buatan Google.")
	fmt.Println("Keunggulan: Bebas Reflection, Type-Safe, dan Terverifikasi saat Compile Time!")
}

Praktik dan kuis bab ini di kelas interaktif →

160. Provider & Constructor Function Pattern

Memahami konsep Provider sebagai fungsi constructor pengembali instance dependensi.

Apa itu Provider di Wire?

Provider adalah fungsi Go biasa yang menghasilkan sebuah nilai atau instance dependensi. Fungsi constructor standar Go (seperti NewUserRepository atau NewDBConnection) secara otomatis dianggap sebagai Provider oleh Google Wire.

📌 Contoh Fungsi Provider:
// Provider DB Connection
func ProvideDB() *sql.DB {
    db, _ := sql.Open("mysql", "root:root@tcp(localhost:3306)/app")
    return db
}

// Provider Repository (Membutuhkan DB)
func ProvideUserRepository(db *sql.DB) *UserRepository {
    return &UserRepository{DB: db}
}

Contoh program

package main

import (
	"fmt"
)

type Database struct{ Dsn string }
type UserRepo struct{ DB *Database }

// Provider 1
func NewDatabase() *Database {
	return &Database{Dsn: "localhost:3306"}
}

// Provider 2 (Membutuhkan *Database)
func NewUserRepo(db *Database) *UserRepo {
	return &UserRepo{DB: db}
}

func main() {
	db := NewDatabase()
	repo := NewUserRepo(db)

	fmt.Println("Repo DB DSN:", repo.DB.Dsn)
}

Praktik dan kuis bab ini di kelas interaktif →

161. Injector Function & File wire.go

Deklarasi fungsi Injector menggunakan `wire.Build(...)` dalam file khusus `injector.go` atau `wire.go`.

Membuat File Injector (`wire.go`)

Injector adalah fungsi yang merangkai provider-provider untuk mengembalikan objek akhir aplikasi. Kita menuliskan fungsi Injector di file wire.go dengan directive build tag khusus //go:build wireinject.

💡 Contoh Kode wire.go:
//go:build wireinject
// +build wireinject

package main

import "github.com/google/wire"

func InitializedUserService() *UserService {
    wire.Build(ProvideDB, ProvideUserRepository, ProvideUserService)
    return nil
}

Saat perintah wire dijalankan di terminal, Wire akan menghasilkan file wire_gen.go berisi kode perangkaian otomatis!

Contoh program

package main

import (
	"fmt"
)

func main() {
	fmt.Println("=== INJECTOR DEMO ===")
	fmt.Println("File wire.go berisi deklarasi wire.Build(Provider1, Provider2...)")
	fmt.Println("Command 'wire' menghasilkan file wire_gen.go!")
}

Praktik dan kuis bab ini di kelas interaktif →

162. Interface Binding dengan wire.Bind

Mengabaikan detail konkret struct dan mengaitkan implementasi struct ke Interface menggunakan `wire.Bind`.

Interface Binding (`wire.Bind`)

Jika sebuah Service membutuhkan dependensi bertipe Interface, sedangkan Provider mengembalikan pointer *StructKonkret, Wire perlu tahu bahwa struct tersebut mengimplementasikan interface tersebut.

📌 Penggunaan wire.Bind:
var UserRepositorySet = wire.NewSet(
    ProvideMySQLUserRepository,
    wire.Bind(new(UserRepository), new(*MySQLUserRepository)),
)

Fungsi wire.Bind(new(TargetInterface), new(*ConcreteStruct)) memberitahu Wire untuk menginjeksi struct konkret sebagai interface target.

Contoh program

package main

import (
	"fmt"
)

type CategoryRepo interface {
	FindAll() []string
}

type MemoryCategoryRepo struct{}

func (m *MemoryCategoryRepo) FindAll() []string {
	return []string{"Elektronik", "Pakaian", "Buku"}
}

func main() {
	var repo CategoryRepo = &MemoryCategoryRepo{}
	fmt.Println("Categories:", repo.FindAll())
}

Praktik dan kuis bab ini di kelas interaktif →

163. Provider Sets (wire.NewSet)

Mengelompokkan provider-provider terkait menjadi satu modul terorganisir menggunakan `wire.NewSet`.

Pengelompokan Provider (Provider Set)

Pada aplikasi berskala besar dengan puluhan modul, mendaftarkan setiap provider satu per satu di `wire.Build(...)` akan sangat panjang. Wire menyediakan wire.NewSet untuk mengelompokkan provider yang saling berhubungan.

💡 Contoh Provider Set Modul User:
var UserSet = wire.NewSet(
    ProvideUserRepository,
    ProvideUserService,
    ProvideUserController,
)

Selanjutnya pada `wire.go` kita cukup memanggil wire.Build(UserSet, OrderSet, DatabaseSet).

Contoh program

package main

import (
	"fmt"
)

func main() {
	fmt.Println("=== PROVIDER SETS DEMO ===")
	fmt.Println("wire.NewSet(Provider1, Provider2, Provider3) merangkai grup provider menjadi modul terstruktur.")
}

Praktik dan kuis bab ini di kelas interaktif →

164. Struct Provider & Property Injection (wire.Struct)

Melakukan injeksi dependensi langsung ke field struct tanpa perlu membuat fungsi constructor manual.

Injeksi Langsung Field Struct (`wire.Struct`)

Jika sebuah struct hanya berfungsi menampung dependensi tanpa ada logika inisialisasi kompleks di constructor, kita dapat menggunakan wire.Struct daripada membuat fungsi New...() secara manual.

📌 Penggunaan wire.Struct:
// Injeksi ke seluruh field struct:
var FooSet = wire.Struct(new(Foo), "*")

// Injeksi spesifik field tertentu saja:
var BarSet = wire.Struct(new(Bar), "MyRepo", "MyLogger")

Contoh program

package main

import (
	"fmt"
)

type DB struct{}
type Logger struct{}

type AppService struct {
	DB     *DB
	Logger *Logger
}

func main() {
	// Simulasi wire.Struct(new(AppService), "*")
	app := &AppService{
		DB:     &DB{},
		Logger: &Logger{},
	}

	fmt.Printf("AppService initialized with DB (%T) and Logger (%T)\n", app.DB, app.Logger)
}

Praktik dan kuis bab ini di kelas interaktif →

165. Cleanup Functions & Error Handling di Wire

Menangani error return dari Provider dan mengelola fungsi penutupan resource (Cleanup/Close) di Wire.

Error Handling & Cleanup Functions

Provider di Wire dapat mengembalikan error atau fungsi cleanup dengan signature func().

💡 Signature Provider dengan Error & Cleanup:
func ProvideFile() (*os.File, func(), error) {
    file, err := os.OpenFile("data.txt", os.O_RDWR, 0644)
    if err != nil {
        return nil, nil, err
    }
    cleanup := func() {
        file.Close() // Dipanggil saat aplikasi shutdown
    }
    return file, cleanup, nil
}

Wire akan secara otomatis meneruskan penanganan error ke Injector dan merangkai fungsi-fungsi cleanup untuk dipanggil di main()!

Contoh program

package main

import (
	"fmt"
)

func InitResource() (string, func(), error) {
	resource := "Connection Established"
	cleanup := func() {
		fmt.Println("🧹 CLEANUP: Resources closed successfully!")
	}
	return resource, cleanup, nil
}

func main() {
	res, cleanup, err := InitResource()
	if err != nil {
		fmt.Println("Error:", err)
		return
	}

	fmt.Println("Resource Status:", res)
	// Eksekusi cleanup di akhir main
	defer cleanup()
}

Praktik dan kuis bab ini di kelas interaktif →