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:

SituasiPilihan
Method perlu mengubah field structPointer receiver
Struct berukuran besar (banyak field)Pointer receiver
Method hanya membaca dataValue receiver
Struct kecil, immutable secara konseptualValue receiver
Tipe apapun di struct sudah ada pointer receiverPointer 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() intstring 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.

Referensi

  1. 1Methods — A Tour of Go
  2. 2Choosing a value or pointer receiver — A Tour of Go
  3. 3Method declarations — The Go Programming Language Specification