Go tidak memiliki kelas, tetapi perilaku tetap dapat ditempelkan pada sebuah tipe melalui metode, yaitu fungsi yang memiliki receiver. Receiver adalah parameter khusus yang menyatakan nilai tempat metode itu dipanggil. Topik ini membahas cara mendeklarasikan metode pada struct, perbedaan receiver nilai dan receiver pointer, serta pedoman memilih keduanya. Setelah membaca topik ini, dampak tiap jenis receiver terhadap data dapat diprediksi, sehingga pemilihannya dilakukan secara sadar dan konsisten.
Metode adalah fungsi dengan receiver#
Sebuah metode dideklarasikan seperti fungsi biasa, tetapi di antara kata kunci func dan nama metode terdapat satu parameter tambahan yang disebut receiver. Bentuknya func (k KartuAnggota) Ringkas() string, yang berarti metode Ringkas menempel pada tipe KartuAnggota, dan di dalam badannya nilai kartu diakses melalui nama k. Pemanggilannya menggunakan notasi titik pada sebuah nilai, sebagaimana program berikut:
package main
import "fmt"
// KartuAnggota menyimpan data kartu anggota perpustakaan.
type KartuAnggota struct {
Nama string
Pinjaman int
}
// ringkas merangkai kartu tanpa mengubah isinya.
func (k KartuAnggota) Ringkas() string {
return fmt.Sprintf("%s, pinjaman aktif %d", k.Nama, k.Pinjaman)
}
// pinjam menambah penghitung pinjaman, sehingga memerlukan receiver pointer.
func (k *KartuAnggota) Pinjam() {
k.Pinjaman++
}
func main() {
kartu := KartuAnggota{Nama: "Dewi", Pinjaman: 0}
fmt.Println(kartu.Ringkas())
kartu.Pinjam()
kartu.Pinjam()
fmt.Println(kartu.Ringkas())
}Dewi, pinjaman aktif 0
Dewi, pinjaman aktif 2Program di atas memuat dua metode dengan dua jenis receiver sekaligus. Metode Ringkas hanya membaca field Nama dan Pinjaman untuk merangkai teks, sedangkan metode Pinjam menaikkan penghitung Pinjaman. Perbedaan jenis receiver pada kedua metode tersebut bukan variasi penulisan semata. Jenis receiver menentukan apakah metode boleh mengubah data struct tempat metode dipanggil. Bagian berikut membahas perbedaan itu satu per satu.
Secara mekanis, metode adalah gula sintaksis untuk fungsi biasa dengan parameter pertama bertipe receiver. Deklarasi func (k KartuAnggota) Ringkas() string setara dengan fungsi func Ringkas(k KartuAnggota) string, dan pemanggilan kartu.Ringkas() setara dengan Ringkas(kartu). Bentuk notasi titik dipertahankan karena tipe yang terikat terlihat jelas pada titik pemanggilannya, sedangkan perilakunya tetap diperiksa oleh kompiler sebagaimana fungsi biasa.
Receiver nilai bekerja pada salinan#
Receiver nilai menyalin nilainya pada setiap pemanggilan, sehingga badan metode bekerja pada salinan, bukan pada struct aslinya. Perubahan apa pun terhadap salinan itu lenyap begitu metode selesai dieksekusi. Program berikut memanggil metode Kalibrasi yang berusaha mengisi field Suhu dengan nol:
package main
import "fmt"
// Sensor menyimpan pengukuran suhu satu alat.
type Sensor struct {
Kode string
Suhu float64
}
// kalibrasi dengan receiver nilai hanya mengubah salinan struct.
func (s Sensor) Kalibrasi() {
s.Suhu = 0
}
func main() {
sensor := Sensor{Kode: "SN-01", Suhu: 25.5}
sensor.Kalibrasi()
fmt.Printf("%+v\n", sensor)
}{Kode:SN-01 Suhu:25.5}Nilai Suhu tetap 25.5 karena penugasan di dalam Kalibrasi hanya menyentuh salinannya. Perilaku ini konsisten dengan aturan bahwa parameter fungsi selalu menerima salinan argumennya, dan receiver diperlakukan sebagai parameter pertama. Receiver nilai dengan demikian cocok untuk metode yang hanya membaca data, misalnya perhitungan, perakitan teks, dan pemeriksaan kondisi.
Receiver pointer bekerja pada struct asli#
Receiver pointer dideklarasikan dengan tipe pointer, misalnya *Sensor, sehingga metode menerima alamat struct asli, bukan salinannya. Melalui alamat itulah perubahan field tercatat pada struct yang benar-benar dipakai pemanggil. Program berikut identik dengan program sebelumnya, kecuali satu barisnya, yaitu tanda bintang pada receiver:
package main
import "fmt"
// Sensor menyimpan pengukuran suhu satu alat.
type Sensor struct {
Kode string
Suhu float64
}
// kalibrasi dengan receiver pointer mengubah struct aslinya.
func (s *Sensor) Kalibrasi() {
s.Suhu = 0
}
func main() {
sensor := Sensor{Kode: "SN-01", Suhu: 25.5}
sensor.Kalibrasi()
fmt.Printf("%+v\n", sensor)
}{Kode:SN-01 Suhu:0}Kali ini Suhu berubah menjadi 0, karena s menunjuk struct sensor yang asli. Kompiler menolong penulisan dengan menyisipkan pengambilan alamat secara otomatis, sehingga sensor.Kalibrasi() pada variabel yang dapat dialamatkan (addressable) setara dengan (&sensor).Kalibrasi(). Bantuan ini berlaku hanya bila variabelnya dapat dialamatkan. Elemen peta dan hasil pemanggilan fungsi tidak dapat dialamatkan, sehingga pemanggilan metode pointer padanya gagal dikompilasi:
// tidak-dijalankan
peta := map[string]Sensor{"SN-01": {Suhu: 25.5}}
peta["SN-01"].Kalibrasi() // gagal dikompilasi: cannot call pointer method Kalibrasi on SensorKapan memilih receiver pointer#
Kedua jenis receiver sama sahnya, sehingga pilihannya menuntut pertimbangan yang tertulis. Tabel berikut merangkum situasi yang lazim beserta pilihannya.
| Situasi | Receiver yang disarankan | Alasan |
|---|---|---|
| Metode perlu mengubah field struct | pointer | Perubahan hanya tercatat melalui alamat struct |
| Struct berukuran besar dan sering dipanggil | pointer | Penyalinan berulang terhindarkan |
| Metode hanya membaca data pada struct kecil | nilai | Salinan murah dan isinya dijamin tidak berubah |
Dari tabel tersebut diperoleh dua pedoman praktis. Pertama, gunakan receiver pointer setiap kali metode mengubah data struct, karena receiver nilai menjanjikan perubahan yang tidak pernah terjadi. Kedua, jangan mencampur kedua jenis receiver pada satu tipe, sebagaimana dianjurkan dokumen Effective Go. Apabila satu metode memerlukan pointer, biasakan seluruh metode tipe itu memakai pointer agar aturan pemanggilannya seragam di seluruh kode.
Pertimbangan efisiensi menjadi alasan tambahan pada struct berukuran besar. Receiver nilai menyalin seluruh field pada setiap pemanggilan, sehingga struct dengan puluhan field yang metodenya dipanggil berulang kali membayar biaya salinan yang sebenarnya tidak diperlukan. Receiver pointer hanya menyalin satu alamat, dan biayanya tetap tanpa memandang ukuran struct yang ditunjuk. Untuk struct kecil dengan metode yang hanya membaca data, selisih biaya tersebut tidak berarti, dan receiver nilai tetap dipilih karena salinannya mencegah perubahan tak sengaja.
Kesalahan umum#
- Mengira metode dengan receiver nilai mengubah data. Penyebabnya adalah sifat salinan pada receiver nilai, dan akibatnya perubahan hilang tanpa pesan error. Perbaikannya, ubah receiver menjadi pointer bila metode memang dimaksudkan untuk mengubah data.
- Memanggil metode pointer pada nilai yang tidak dapat dialamatkan, misalnya elemen peta atau hasil fungsi. Kompilasi gagal dengan pesan
cannot call pointer method ... on .... Perbaikannya, simpan nilai tersebut ke variabel lokal terlebih dahulu, lalu panggil metodenya pada variabel itu. - Menamai receiver dengan
thisatauself, kebiasaan yang terbawa dari bahasa berorientasi objek. Konvensi Go menamai receiver dengan singkatan nama tipe, sepertikuntukKartuAnggotadansuntukSensor, sehingga kodenya tetap ringkas tanpa menimbulkan kesan kelas.
Ringkasan#
- Metode adalah fungsi dengan receiver, yaitu parameter pertama yang mengikat fungsi ke sebuah tipe.
- Receiver nilai bekerja pada salinan, sehingga perubahan fieldnya tidak berpengaruh pada struct asli.
- Receiver pointer bekerja pada struct asli melalui alamatnya dan wajib dipakai untuk perubahan data.
- Gunakan satu jenis receiver untuk seluruh metode sebuah tipe, dengan pointer sebagai pilihan ketika ragu.
Referensi#
- Effective Go — Go Documentation — Bagian methods beserta pedoman pemilihan receiver nilai dan pointer.
- A Tour of Go: Methods — Go Documentation — Tur resmi konsep metode Go beserta latihan interaktifnya.
- The Go Programming Language Specification — The Go Project — Aturan resmi deklarasi metode dan pemanggilannya.