Struct, method & receiver
Struct adalah kumpulan field; method adalah fungsi yang menempel pada tipe. Tidak ada pewarisan, tidak ada konstruktor bawaan — dan pilihan receiver menentukan apakah perubahanmu tersimpan.
Intisari
- Receiver pointer (
*T) kalau method mengubah isi, atau kalau struct-nya besar. Receiver nilai (T) kalau tidak. - Jangan campur keduanya dalam satu tipe. Kalau ragu, pakai pointer untuk semuanya.
- Tidak ada konstruktor. Konvensinya fungsi
NewatauNewXxxyang mengembalikan*T. - Embedding bukan pewarisan — ia mempromosikan field dan method, tanpa polimorfisme.
- Method boleh dipasang ke tipe apa pun yang kamu definisikan, termasuk
type Rupiah int64.
Struct dan konstruktornya
type Pesanan struct {
ID int64
Pelanggan string
Item []Item
Total Rupiah
Dibuat time.Time
}
// Tidak ada kata kunci "constructor". Konvensinya fungsi New yang
// mengembalikan pointer, dan yang mengisi field yang tidak boleh kosong.
func NewPesanan(pelanggan string) *Pesanan {
return &Pesanan{
Pelanggan: pelanggan,
Item: make([]Item, 0, 4),
Dibuat: time.Now(),
}
}
p := NewPesanan("Ani")
p2 := Pesanan{Pelanggan: "Budi"} // literal — sah, tapi Item-nya nil
var p3 Pesanan // nilai nol — semua field bernilai nol
Selalu isi field dengan nama. Pesanan{1, "Ani", nil, 0, waktu} juga sah, tapi
begitu ada yang menambah field di tengah struct, seluruh pemakaian positional itu rusak — atau lebih buruk,
tetap kompilasi dengan nilai yang bergeser. go vet memperingatkan untuk struct dari
paket lain; untuk struct sendiri, disiplin sendiri yang menjaga.
Receiver: nilai atau pointer
// Receiver NILAI — menerima salinan. Perubahan tidak tersimpan.
func (p Pesanan) Ringkasan() string {
return fmt.Sprintf("#%d %s", p.ID, p.Pelanggan)
}
// Receiver POINTER — menerima alamat. Perubahan tersimpan.
func (p *Pesanan) Tambah(i Item) {
p.Item = append(p.Item, i)
p.Total += i.Harga
}
p := NewPesanan("Ani")
p.Tambah(Item{Harga: 25000}) // Go otomatis mengambil alamatnya: (&p).Tambah(...)
| Pakai receiver… | Kalau… |
|---|---|
*T (pointer) | Method mengubah isi |
*T | Struct-nya besar (menyalinnya mahal) |
*T | Struct berisi sync.Mutex atau tipe lain yang tidak boleh disalin |
T (nilai) | Tipe kecil dan tidak berubah — time.Time, Rupiah, koordinat |
T | Kamu memang ingin semantik nilai (setiap salinan berdiri sendiri) |
Aturan yang menghindarkan seluruh kelas bug: jangan campur. Kalau satu method memakai
*T, buat semuanya *T. Tipe dengan receiver campuran menghasilkan situasi di mana
T memenuhi sebuah interface tapi *T tidak (atau sebaliknya), dan pesan
errornya sangat membingungkan. Kalau ragu di awal: pointer untuk semuanya.
Jebakan receiver nilai yang mengubah slice
// SALAH — terlihat bekerja, tapi tidak
func (p Pesanan) TambahSalah(i Item) {
p.Item = append(p.Item, i) // p adalah salinan; hilang saat fungsi selesai
}
// Lebih licik: slice-nya SAMA, jadi mengubah ELEMEN justru terlihat
func (p Pesanan) UbahHarga(idx int, h Rupiah) {
p.Item[idx].Harga = h // ← ini BERPENGARUH ke pemanggil
}
Salinan struct menyalin header slice, bukan datanya. Jadi append hilang tapi
perubahan elemen tersimpan. Perilaku setengah-setengah inilah alasan aturan "jangan campur" tadi.
Embedding: komposisi, bukan pewarisan
type Model struct {
ID int64
Dibuat time.Time
Diubah time.Time
}
func (m *Model) Sentuh() { m.Diubah = time.Now() }
type Produk struct {
Model // ← embedded: tanpa nama field
Nama string
Harga Rupiah
}
p := Produk{}
p.ID = 7 // dipromosikan dari Model
p.Sentuh() // method Model ikut dipromosikan
p.Model.ID = 7 // bentuk panjangnya, selalu tersedia
Ini bukan pewarisan. Produk bukan sebuah Model: kamu tidak
bisa mengoper Produk ke fungsi yang meminta Model, dan tidak ada
override — kalau Produk punya method Sentuh sendiri, method
Model hanya tertutup, bukan tergantikan secara polimorfik. Yang embedding berikan cuma
promosi nama. Polimorfisme di Go datang dari interface, bukan dari sini.
Method pada tipe apa pun
// Tipe bernama di atas int64 — bukan alias, tipe yang berbeda.
type Rupiah int64
func (r Rupiah) String() string {
return fmt.Sprintf("Rp%s", tandaRibuan(int64(r)))
}
type IDPengguna int64
type IDProduk int64
// Compiler menolak menukar keduanya — bug yang di banyak bahasa
// baru ketahuan di produksi.
func Ambil(u IDPengguna) { }
Ambil(IDProduk(7)) // gagal kompilasi
Tipe bernama di atas tipe primitif adalah salah satu alat paling murah untuk mencegah kesalahan di aplikasi besar. Uang, ID, satuan waktu, dan kode negara semua layak punya tipe sendiri — biayanya satu baris, dan compiler yang menegakkannya selamanya.
Field yang tidak diekspor menjaga invarian
type Saldo struct {
nilai Rupiah // huruf kecil: tidak terlihat dari paket lain
}
func (s *Saldo) Tarik(n Rupiah) error {
if n <= 0 {
return errors.New("jumlah harus positif")
}
if n > s.nilai {
return ErrSaldoKurang
}
s.nilai -= n
return nil
}
Karena nilai huruf kecil, tidak ada kode di luar paket ini yang bisa membuat saldo negatif.
Ini padanan Go untuk enkapsulasi: batasnya paket, bukan tipe. Di dalam satu paket, semua
kode bisa saling menyentuh field privat — dan itu disengaja, karena paket adalah unit desain di Go.
Ringkasan keputusan
| Pertanyaan | Jawaban standar |
|---|---|
Kembalikan T atau *T dari New? | *T, kecuali tipenya kecil dan tidak berubah |
Perlu getter/setter? | Tidak, kecuali ada aturan yang harus dijaga. Field publik itu wajar di Go |
| Nama getter | p.Nama(), bukan p.GetNama() |
| Struct kosong sebagai penanda | struct{} — memakai nol byte, dipakai untuk set dan sinyal channel |
Latihan: buat tipe Keranjang dengan field tak diekspor item []Item,
method Tambah (pointer receiver) dan Total (nilai receiver). Lalu tulis method
Tambah versi value receiver, panggil dari main, dan buktikan sendiri
bahwa keranjangnya tetap kosong. Terakhir, tambahkan tipe Rupiah dengan method
String() dan lihat fmt.Println memakainya otomatis.
Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.