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.
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.Readerpunya satu method dan dipakai di seluruh ekosistem. - "Accept interfaces, return structs" — terima yang abstrak, kembalikan yang konkret.
- Interface bernilai
niltapi 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 implementasinya | Paket postgres cuma mengembalikan struct konkret |
| Pemakai mengimpor interface dari paket penyedia | Pemakai mendeklarasikan sendiri interface berisi method yang ia butuhkan |
| Interface besar, mengikuti seluruh API implementasi | Interface 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.