← Semua pembelajaran / Go Nol → Enterprise
Fase 1 · Tipe, Interface & Error

Interface — kontrak yang implisit

Interface Go dipenuhi secara implisit: sebuah tipe tidak perlu tahu interface apa pun yang ia penuhi. Konsekuensinya, interface dideklarasikan di sisi yang memakai, bukan di sisi yang menyediakan.

Sumber asli go.dev Resmi Rangkuman ~9 menit baca

Intisari

  • Tidak ada implements. Punya method-nya = memenuhi interface-nya.
  • Deklarasikan interface di paket yang memakainya, bukan di paket yang mengimplementasikannya. Ini pembalikan terpenting di Go.
  • Interface kecil menang: io.Reader punya satu method dan dipakai di seluruh ekosistem.
  • "Accept interfaces, return structs" — terima yang abstrak, kembalikan yang konkret.
  • Interface bernilai nil tapi berisi tipe konkret tidak sama dengan nil. Ini jebakan Go yang paling terkenal.

Implisit, dan kenapa itu penting

// Di pustaka standar:
type Writer interface {
	Write(p []byte) (n int, err error)
}

// Tipe apa pun yang punya method itu — TANPA mendaftar ke mana pun —
// bisa dipakai di mana Writer diminta.
type PenghitungByte struct{ n int64 }

func (p *PenghitungByte) Write(b []byte) (int, error) {
	p.n += int64(len(b))
	return len(b), nil
}

var w io.Writer = &PenghitungByte{}   // sah

Karena tidak ada deklarasi implements, kamu bisa membuat tipe milik pustaka pihak ketiga memenuhi interface yang kamu definisikan hari ini — tanpa menyentuh kode mereka. Ini yang membuat tes di Go jarang butuh kerangka mocking.

Aturan terpenting: interface milik pemakai

Kebiasaan dari Java/C#Cara Go
Paket repository mendeklarasikan ProdukRepository lalu implementasinyaPaket postgres cuma mengembalikan struct konkret
Pemakai mengimpor interface dari paket penyediaPemakai mendeklarasikan sendiri interface berisi method yang ia butuhkan
Interface besar, mengikuti seluruh API implementasiInterface kecil, hanya berisi yang benar-benar dipakai
// internal/pesanan/layanan.go — PEMAKAI mendeklarasikan kebutuhannya.
// Perhatikan: paket ini tidak mengimpor paket postgres sama sekali.
type PenyimpanProduk interface {
	AmbilProduk(ctx context.Context, id int64) (Produk, error)
}

type Layanan struct {
	produk PenyimpanProduk
}

func NewLayanan(p PenyimpanProduk) *Layanan { return &Layanan{produk: p} }
// internal/produk/postgres.go — PENYEDIA tidak tahu interface itu ada.
type Postgres struct{ db *pgxpool.Pool }

func (p *Postgres) AmbilProduk(ctx context.Context, id int64) (Produk, error) { ... }

Kenapa ini bukan sekadar selera. Dengan pola ini, internal/pesanan tidak bergantung pada paket database sama sekali — arah ketergantungan menunjuk ke dalam, ke domain. Tesnya pun tidak butuh database: cukup satu struct kecil di berkas tes yang punya method AmbilProduk. Dan kalau suatu hari penyimpanannya pindah ke layanan lain, paket domain tidak berubah satu baris pun.

Kecil itu menang

type Reader interface { Read(p []byte) (int, error) }
type Writer interface { Write(p []byte) (int, error) }
type Closer interface { Close() error }

// Digabung lewat embedding
type ReadCloser interface {
	Reader
	Closer
}

io.Reader punya satu method, dan karena itu berkas, koneksi jaringan, body HTTP, buffer memori, arsip gzip, dan hasil enkripsi semuanya bisa dipakai di tempat yang sama. Semakin banyak method sebuah interface, semakin sedikit tipe yang bisa memenuhinya, dan semakin kecil manfaatnya.

Pedoman yang dipakai tim Go: "The bigger the interface, the weaker the abstraction." Interface dengan lebih dari tiga method biasanya tanda bahwa yang kamu butuhkan sebenarnya struct konkret, bukan abstraksi.

Accept interfaces, return structs

// ✅ menerima abstrak, mengembalikan konkret
func NewLayanan(db PenyimpanProduk, log *slog.Logger) *Layanan

// ❌ mengembalikan interface menyembunyikan kemampuan dan menyulitkan pemakai
func NewLayanan(...) LayananInterface

Mengembalikan struct konkret membiarkan pemakai memutuskan sendiri abstraksi apa yang ia butuhkan. Mengembalikan interface memaksa semua pemakai memakai abstraksimu — termasuk yang butuh satu method tambahan yang kebetulan tidak masuk daftar.

Jebakan: interface nil yang tidak nil

type KesalahanSaya struct{}
func (*KesalahanSaya) Error() string { return "gagal" }

func bermasalah() error {
	var e *KesalahanSaya = nil   // pointer nil
	return e                      // dibungkus jadi interface error
}

if bermasalah() != nil {
	fmt.Println("MASUK SINI")    // ← ya, masuk. Meski nilainya nil.
}

Nilai interface terdiri dari dua hal: tipe dan nilai. Di atas, nilainya nil tapi tipenya *KesalahanSaya — jadi interface-nya tidak nil. Ini bug Go paling terkenal, dan bentuknya selalu sama: fungsi mengembalikan tipe error konkret alih-alih error.

Aturan pencegahnya satu kalimat: deklarasikan nilai kembalian sebagai error, dan kembalikan nil secara harfiah — jangan pernah mengembalikan variabel bertipe pointer konkret ke posisi error. Linter nilness dan golangci-lint (Fase 8) menangkap sebagian besar kasusnya.

Menguji tipe di balik interface

// type assertion — bentuk "comma ok" supaya tidak panik
if pg, ok := penyimpan.(*Postgres); ok {
	pg.Statistik()
}

// type switch
switch v := nilai.(type) {
case string:
	fmt.Println("teks", v)
case int, int64:
	fmt.Println("angka", v)
case error:
	fmt.Println("error", v)
default:
	fmt.Printf("tipe tak terduga %T\n", v)
}

any adalah alias untuk interface{} sejak Go 1.18 — makna keduanya identik. Tapi sebuah nilai any berarti compiler berhenti membantumu: setiap pemakaian butuh assertion, dan setiap assertion bisa gagal saat program berjalan. Generic (materi berikutnya) sering jadi jawaban yang lebih baik.

Memastikan implementasi saat kompilasi

// Baris ini tidak menghasilkan apa pun saat runtime, tapi membuat compiler
// gagal kalau *Postgres berhenti memenuhi PenyimpanProduk.
var _ PenyimpanProduk = (*Postgres)(nil)

Tanpa baris ini, kesalahan baru muncul di tempat yang jauh — saat seseorang mencoba mengoper *Postgres ke konstruktor, dengan pesan error yang menyebut paket lain. Satu baris ini membuat kegagalan muncul tepat di berkas yang salah.

Latihan: tulis fungsi Salin(dst io.Writer, src io.Reader) error, lalu panggil tiga kali dengan pasangan berbeda: berkas ke berkas, strings.NewReader ke os.Stdout, dan bytes.Buffer ke bytes.Buffer. Satu fungsi, tiga dunia yang berbeda — tanpa satu pun deklarasi implements.

Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.