Go Structs Interfaces Pointers Methods
Selesai membaca dokumentasi ini?
Kembali ke rujukan utama Go (Golang) untuk melanjutkan topik lainnya.
Kembali ke rujukan utama Go (Golang) untuk melanjutkan topik lainnya.
Di Go, struct, interface, pointer, dan method sering dipakai bareng karena mereka membentuk cara kita memodelkan data dan perilakunya.
Kalau dibaca sebagai satu sistem, struct itu bentuk datanya, method itu perilaku, pointer itu cara mengakses/mengubah nilai, dan interface itu batas decoupling.
Pakai struct saat data punya beberapa atribut yang memang satu unit.
Contoh:
type User struct {
ID int
Name string
Email string
}struct cocok untuk:
User, Order, InvoiceConfig, OptionsKalau data cuma satu nilai sederhana dan tidak punya makna gabungan, jangan dipaksakan jadi struct. Misalnya integer count, boolean flag, atau string tunggal sering lebih jelas sebagai nilai biasa.
Method adalah function yang punya receiver.
func (u User) DisplayName() string {
return u.Name
}Receiver menentukan tipe yang “memiliki” method itu. Karena method terpasang pada tipe, method set ini sangat penting saat kita pakai interface.
Ini bagian yang paling sering bikin bingung, dan paling sering jadi sumber bug yang diam-diam.
func (u User) DisplayName() string {
return u.Name
}Value receiver menerima salinan nilai. Cocok kalau:
func (u *User) Rename(name string) {
u.Name = name
}Pointer receiver menerima alamat nilai asli. Cocok kalau:
Pakai pointer receiver kalau salah satu dari ini benar:
Pakai value receiver kalau:
Kalau sebuah tipe punya beberapa method dan sebagian besar perlu pointer receiver, biasanya lebih aman membuat semua method receiver-nya pointer. Ini menghindari campuran method set yang membingungkan saat tipe itu dipakai sebagai interface.
Di Go, banyak tipe dirancang supaya zero value-nya langsung berguna.
Contoh:
var u User
fmt.Println(u.Name) // string kosong, bukan panicNamun zero value yang “aman” tidak selalu berarti zero value yang “siap pakai” untuk semua operasi.
int → 0string → ""bool → falseslice → nil slice yang bisa di-appendmap → nil, tapi belum bisa di-assign sebelum makestruct → field-field default zero valuemap nil tidak bisa langsung ditulisvar m map[string]int
m["a"] = 1 // panicHarus diinisialisasi dulu:
m := make(map[string]int)pointer nil bisa panic saat dereferencevar u *User
fmt.Println(u.Name) // panicstruct zero value belum tentu valid secara domainMisalnya Config{} mungkin secara sintaks valid, tapi secara bisnis bisa belum lengkap karena field wajib masih kosong.
Kalau kamu mendesain tipe, usahakan zero value-nya:
Kalau zero value tidak valid, dokumentasikan dengan tegas dan sediakan constructor.
Interface mendefinisikan perilaku, bukan bentuk konkret.
type Notifier interface {
Notify(message string) error
}Tipe mana pun yang punya method Notify(string) error otomatis memenuhi interface ini, tanpa deklarasi eksplisit.
Itu membuat interface sangat cocok untuk decoupling.
Decoupling artinya kode kita bergantung pada perilaku, bukan implementasi spesifik.
Contoh:
type Notifier interface {
Notify(message string) error
}
type Service struct {
notifier Notifier
}
func NewService(n Notifier) *Service {
return
Dengan pola ini, Service tidak peduli apakah notifikasinya lewat email, Slack, Telegram, atau mock untuk testing.
Pakai interface saat:
Jangan bikin interface terlalu cepat kalau:
Interface yang terlalu dini sering bikin kode lebih banyak, tapi tidak lebih jelas.
Method set menentukan method apa saja yang dimiliki suatu tipe untuk keperluan interface satisfaction.
Aturannya penting:
T masuk ke method set dari T*T masuk ke method set dari *T*T punya method set yang lebih luas daripada TKalau interface butuh method yang hanya ada di pointer receiver, maka nilai T biasa tidak memenuhi interface itu.
Contoh:
type Greeter interface {
Greet() string
}
type Person struct {
Name string
}
func (p *Person) Greet() string {
return "Halo, " + p.Name
Di sini:
*Person memenuhi GreeterPerson tidak memenuhi Greetervar g Greeter
p := Person{Name: "Dina"}
// g = p // tidak bisa kalau Greet hanya ada di *Person
g = &p // bisaIni sering muncul saat kode terlihat “punya method”, tetapi ternyata yang punya method itu pointer-nya.
Embedding bisa memperluas method yang terlihat, tetapi arah pointer/value tetap penting. Saat interface gagal terpenuhi, cek receiver method-nya dulu, bukan cuma nama method-nya.
Ini salah satu sumber bug paling licin di Go.
var p *Person = nil
var g Greeter = pg di sini bukan nil interface, karena interface-nya berisi tipe *Person dan nilai nil. Akibatnya pengecekan g == nil bisa false walau isi konkretnya nil.
Praktiknya:
Kalau ragu, tanya tiga hal:
Kalau jawabannya ya untuk nomor 1 atau 2, pointer receiver biasanya pilihan yang lebih aman.
type Counter struct {
n int
}
func (c *Counter) Inc() {
c.n++
}
func (c Counter) Value() int {
return c.n
Keterangan:
Inc() perlu pointer receiver karena mutasi stateValue() cukup value receiver karena hanya bacaCounter dan *Counter sama-sama bisa dipakai sebagai ValueReader karena Value() ada di method set keduanyaContoh pemakaian:
c := Counter{}
c.Inc()
fmt.Println(c.Value())
var r ValueReader = c
fmt.Println(r.Value())Kalau kode Go terasa “aneh”, biasanya urutannya begini:
T atau *T