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

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.

Sumber asli go.dev Resmi Rangkuman ~8 menit baca

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 New atau NewXxx yang 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
*TStruct-nya besar (menyalinnya mahal)
*TStruct berisi sync.Mutex atau tipe lain yang tidak boleh disalin
T (nilai)Tipe kecil dan tidak berubah — time.Time, Rupiah, koordinat
TKamu 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

PertanyaanJawaban 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 getterp.Nama(), bukan p.GetNama()
Struct kosong sebagai penandastruct{} — 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.