BAB 18: Method — Memberi Perilaku pada Struct
Pelajari cara mendefinisikan method di Go, perbedaan value dan pointer receiver, serta kapan memilih masing-masingnya.
Di bab sebelumnya, struct Pengguna sudah bisa menyimpan data dan bahkan punya beberapa method sederhana seperti Sapa() dan UlangTahun(). Tapi kita belum benar-benar menggali apa itu method, mengapa ada dua jenis receiver, dan bagaimana memilihnya dengan tepat. Ketika programmu mulai tumbuh dan struct-mu makin banyak method, keputusan ini jadi sangat penting.
Method bukan sekadar function yang menempel pada struct. Method adalah cara Go mewujudkan enkapsulasi — mengikat perilaku ke data, sehingga keduanya tidak terpencar.
Apa Itu Method?
Method adalah function yang dideklarasikan dengan receiver — sebuah parameter tambahan sebelum nama function yang menentukan tipe mana yang memiliki method ini. Bedanya dengan function biasa cukup tipis secara sintaks, tapi signifikan secara konsep.
// function biasa
func sapa(nama string) string {
return "Halo, " + nama
}
// method — dimiliki oleh tipe Pengguna
func (p Pengguna) Sapa() string {
return "Halo, " + p.Nama
}
Keduanya melakukan hal serupa, tapi method dipanggil dengan notasi titik pada instance: p.Sapa(). Ini membuat kode lebih ekspresif — alih-alih sapa(p.Nama), kamu menulis p.Sapa(), yang secara natural terbaca sebagai “panggil Sapa pada Pengguna ini.”
Mendefinisikan Method Pertama
Mari kita mulai dari struct Pengguna yang sudah dibangun di bab sebelumnya dan tambahkan beberapa method yang lebih bermakna.
// main.go
package main
import "fmt"
type Pengguna struct {
Nama string
Email string
Usia int
}
func (p Pengguna) Sapa() string {
return fmt.Sprintf("Halo, nama saya %s.", p.Nama)
}
func (p Pengguna) SudahDewasa() bool {
return p.Usia >= 18
}
func main() {
p := Pengguna{Nama: "Rina", Email: "rina@contoh.com", Usia: 17}
fmt.Println(p.Sapa())
fmt.Println("Sudah dewasa:", p.SudahDewasa())
}
Halo, nama saya Rina.
Sudah dewasa: false
Sapa() dan SudahDewasa() adalah value receiver methods — (p Pengguna) berarti method menerima salinan dari Pengguna, bukan struct aslinya. Perubahan apapun pada p di dalam method tidak akan mempengaruhi variabel di luar.
Value Receiver vs Pointer Receiver
Ini adalah keputusan paling penting saat mendefinisikan method. Go menyediakan dua pilihan, dan masing-masing punya konsekuensi yang berbeda.
Value Receiver — Bekerja pada Salinan
// main.go
package main
import "fmt"
type Pengguna struct {
Nama string
Email string
Usia int
}
// value receiver — perubahan tidak tersimpan
func (p Pengguna) CobaNaikUsia() {
p.Usia++
fmt.Println("Di dalam method:", p.Usia)
}
func main() {
p := Pengguna{Nama: "Rina", Email: "rina@contoh.com", Usia: 28}
p.CobaNaikUsia()
fmt.Println("Di luar method:", p.Usia)
}
Di dalam method: 29
Di luar method: 28
p.Usia di luar method tetap 28. Method menerima salinan, jadi p.Usia++ hanya memodifikasi salinan lokal yang hilang saat method selesai.
Pointer Receiver — Bekerja pada Aslinya
// main.go
package main
import "fmt"
type Pengguna struct {
Nama string
Email string
Usia int
}
// pointer receiver — perubahan tersimpan
func (p *Pengguna) NaikUsia() {
p.Usia++
}
func main() {
p := Pengguna{Nama: "Rina", Email: "rina@contoh.com", Usia: 28}
p.NaikUsia()
fmt.Println("Usia setelah method:", p.Usia)
}
Usia setelah method: 29
Dengan (p *Pengguna), method menerima alamat memori dari struct asli. Memodifikasi p.Usia di dalam method secara langsung memodifikasi struct di luar.
Go secara otomatis mengkonversi p.NaikUsia() menjadi (&p).NaikUsia() ketika NaikUsia membutuhkan pointer receiver dan p adalah value. Kamu tidak perlu menulis (&p).NaikUsia() secara manual.
Kapan Pilih Mana?
Keputusan ini mempengaruhi kebenaran dan performa kode. Berikut panduan yang dipakai komunitas Go:
| Situasi | Pilihan |
|---|---|
| Method perlu mengubah field struct | Pointer receiver |
| Struct berukuran besar (banyak field) | Pointer receiver |
| Method hanya membaca data | Value receiver |
| Struct kecil, immutable secara konseptual | Value receiver |
| Tipe apapun di struct sudah ada pointer receiver | Pointer receiver (konsisten) |
Aturan konsistensi adalah yang paling penting: jika satu method pada suatu tipe menggunakan pointer receiver, gunakan pointer receiver untuk semua method-nya. Mencampur keduanya membingungkan dan bisa menyebabkan bug yang sulit dilacak.
// main.go
package main
import "fmt"
type Pengguna struct {
Nama string
Email string
Usia int
}
// konsisten: semua pakai pointer receiver
func (p *Pengguna) Sapa() string {
return fmt.Sprintf("Halo, saya %s.", p.Nama)
}
func (p *Pengguna) NaikUsia() {
p.Usia++
}
func (p *Pengguna) GantiEmail(email string) {
p.Email = email
}
func main() {
p := &Pengguna{Nama: "Rina", Email: "rina@contoh.com", Usia: 28}
p.NaikUsia()
p.GantiEmail("rina.baru@contoh.com")
fmt.Println(p.Sapa())
fmt.Printf("Usia: %d, Email: %s\n", p.Usia, p.Email)
}
Halo, saya Rina.
Usia: 29, Email: rina.baru@contoh.com
Perhatikan bahwa p sekarang dideklarasikan sebagai *Pengguna (pointer) — ini lebih idiomatis saat semua method menggunakan pointer receiver.
Method pada Tipe Non-Struct
Method tidak hanya bisa menempel pada struct. Di Go, method bisa didefinisikan pada tipe apapun yang didefinisikan dalam package yang sama — termasuk tipe alias dari tipe primitif.
// main.go
package main
import "fmt"
type Celsius float64
type Fahrenheit float64
func (c Celsius) ToFahrenheit() Fahrenheit {
return Fahrenheit(c*9/5 + 32)
}
func (f Fahrenheit) ToCelsius() Celsius {
return Celsius((f - 32) * 5 / 9)
}
func main() {
suhu := Celsius(100)
fmt.Printf("%.1f°C = %.1f°F\n", suhu, suhu.ToFahrenheit())
dingin := Fahrenheit(32)
fmt.Printf("%.1f°F = %.1f°C\n", dingin, dingin.ToCelsius())
}
100.0°C = 212.0°F
32.0°F = 0.0°C
Ini berguna ketika kamu ingin menambahkan perilaku pada tipe yang secara konseptual berbeda, meskipun representasi dasarnya sama — Celsius dan Fahrenheit sama-sama float64, tapi mereka bukan hal yang sama.
Method tidak bisa didefinisikan pada tipe dari package lain. Kamu tidak bisa menulis func (s string) Huruf() int — string bukan tipe yang kamu definisikan. Solusinya adalah membuat tipe alias seperti type MyString string.
Memanggil Method secara Berantai
Jika setiap method mengembalikan pointer ke struct yang sama, kamu bisa memanggil method secara berantai dalam satu ekspresi — pola ini sering disebut method chaining atau builder pattern.
// main.go
package main
import "fmt"
type Builder struct {
Nama string
Kota string
Umur int
}
func (b *Builder) SetNama(nama string) *Builder {
b.Nama = nama
return b
}
func (b *Builder) SetKota(kota string) *Builder {
b.Kota = kota
return b
}
func (b *Builder) SetUmur(umur int) *Builder {
b.Umur = umur
return b
}
func (b *Builder) Cetak() {
fmt.Printf("%s, %d tahun, dari %s\n", b.Nama, b.Umur, b.Kota)
}
func main() {
b := &Builder{}
b.SetNama("Budi").SetKota("Surabaya").SetUmur(30).Cetak()
}
Budi, 30 tahun, dari Surabaya
Latihan
Latihan 1 — Rekening Bank:
Buat struct RekeningBank dengan field Pemilik string, Saldo float64. Tambahkan method Setor(jumlah float64), Tarik(jumlah float64) error (kembalikan error jika saldo tidak cukup), dan CetakSaldo(). Gunakan pointer receiver secara konsisten.
Latihan 2 — Termometer:
Buat tipe Kelvin float64. Tambahkan method ToCelsius() Celsius dan ToFahrenheit() Fahrenheit yang melengkapi konversi yang sudah ada di contoh tadi. Uji konversi dari titik nol absolut (0K = -273.15°C).
Latihan 3 — Builder Pattern:
Buat struct KonfigurasiServer dengan field Host string, Port int, TLS bool, Timeout int. Implementasikan builder pattern sehingga konfigurasi bisa dibuat dengan cara berantai: NewConfig().SetHost("localhost").SetPort(8080).SetTLS(true).Build().
Method adalah cara Go mengikat perilaku ke data — dan dengan itu, struct menjadi lebih dari sekadar wadah. Tapi ada satu hal yang belum kita bahas: field dan method yang kamu definisikan di struct ini, siapa saja yang boleh mengaksesnya? Kalau programmu terdiri dari beberapa package, ini menjadi pertanyaan yang sangat relevan.