Dynamic Dispatch
Secara sederhana, Dynamic Dispatch adalah mekanisme di mana metode (fungsi) yang akan dieksekusi ditentukan pada saat runtime (saat program berjalan), bukan pada saat compile time (saat program dikompilasi). Di Go, mekanisme ini diwujudkan melalui penggunaan interface.
## 1. Konsep Dasar: Kontrak dan Implementasi
Bayangkan Anda memiliki remote control universal. Remote ini punya tombol Power()
, VolumeUp()
, dan VolumeDown()
. Remote ini tidak peduli apakah Anda mengarahkannya ke TV, AC, atau sound system. Selama perangkat tersebut "mengerti" sinyal dari tombol-tombol itu, perangkat akan merespons dengan benar.
Dalam analogi ini:
Remote Control Universal adalah Interface. Ia mendefinisikan sebuah "kontrak" atau serangkaian perilaku (metode) yang harus ada, tetapi tidak peduli bagaimana perilaku itu diimplementasikan.
TV, AC, dan Sound System adalah Tipe Konkret (seperti
struct
). Masing-masing mengimplementasikan perilaku dari remote tersebut dengan caranya sendiri.
Di Go:
Interface mendefinisikan method set (kumpulan metode).
Struct (atau tipe lainnya) secara implisit memenuhi interface tersebut jika ia mengimplementasikan semua metode yang ada di dalam interface.
Contoh Dasar
Mari kita buat interface Bentuk
yang memiliki satu metode, HitungLuas()
.
package main
import "fmt"
// 1. Interface (Kontrak)
// Setiap tipe yang ingin dianggap sebagai 'Bentuk' harus memiliki metode HitungLuas() float64.
type Bentuk interface {
HitungLuas() float64
}
// 2. Tipe Konkret: Persegi
type Persegi struct {
Sisi float64
}
// Persegi mengimplementasikan interface Bentuk karena memiliki metode HitungLuas().
func (p Persegi) HitungLuas() float64 {
return p.Sisi * p.Sisi
}
// 3. Tipe Konkret: Lingkaran
type Lingkaran struct {
Radius float64
}
// Lingkaran juga mengimplementasikan interface Bentuk.
func (l Lingkaran) HitungLuas() float64 {
return 3.14 * l.Radius * l.Radius
}
// 4. Fungsi yang Menggunakan Interface (Dynamic Dispatch Terjadi di Sini)
// Fungsi ini tidak peduli apakah yang masuk adalah Persegi atau Lingkaran.
// Selama ia adalah sebuah 'Bentuk', fungsi ini bisa memanggil HitungLuas().
func CetakLuas(b Bentuk) {
fmt.Printf("Luas bentuk ini adalah: %0.2f\n", b.HitungLuas())
}
func main() {
persegi := Persegi{Sisi: 10}
lingkaran := Lingkaran{Radius: 5}
// Panggilan CetakLuas(persegi)
// Pada runtime, Go melihat bahwa 'b' berisi tipe Persegi,
// lalu memanggil metode HitungLuas() milik Persegi.
CetakLuas(persegi)
// Panggilan CetakLuas(lingkaran)
// Pada runtime, Go melihat bahwa 'b' berisi tipe Lingkaran,
// lalu memanggil metode HitungLuas() milik Lingkaran.
CetakLuas(lingkaran)
}
Pada fungsi CetakLuas(b Bentuk)
, pemanggilan b.HitungLuas()
adalah inti dari dynamic dispatch. Kompiler tidak tahu apakah akan memanggil Persegi.HitungLuas()
atau Lingkaran.HitungLuas()
. Keputusan ini dibuat saat program berjalan, berdasarkan tipe konkret aktual yang disimpan di dalam variabel interface b
.
## 2. Cara Kerja di Balik Layar: Representasi Interface
Untuk memahami bagaimana Go melakukan ini, kita perlu tahu bahwa variabel interface di Go sebenarnya adalah sebuah struct dengan dua pointer.
Sebuah nilai interface terdiri dari dua komponen:
Tipe: Pointer ke informasi tipe konkret yang disimpan (misalnya,
*main.Persegi
).Data: Pointer ke data dari nilai konkret tersebut (misalnya, pointer ke lokasi memori struct
persegi
).
Struktur internal ini sering disebut iface
(untuk interface non-kosong) atau eface
(untuk interface kosong interface{}
).
Mari kita fokus pada iface
(untuk interface seperti Bentuk
):
tab
: Pointer ke sebuah struktur bernama itab (interface table).data
: Pointer ke nilai konkretnya.
Apa itu itab?
itab adalah kunci dari dynamic dispatch. Ia berisi:
Informasi tentang tipe konkret (seperti
Persegi
).Pointer ke implementasi metode yang dibutuhkan oleh interface tersebut. Ini pada dasarnya adalah tabel lookup (sering disebut vtable di bahasa lain) yang memetakan setiap metode di interface ke fungsi implementasi yang sesuai pada tipe konkret.
Ketika Anda memanggil b.HitungLuas()
, inilah yang terjadi:
Go melihat ke dalam variabel interface
b
.Ia mengikuti pointer
tab
untuk menemukanitab
yang sesuai.Di dalam
itab
, ia mencari entri untuk metodeHitungLuas()
.Entri ini berisi alamat (pointer) dari fungsi
Persegi.HitungLuas()
(atauLingkaran.HitungLuas()
).Go kemudian memanggil fungsi tersebut, dengan memberikan pointer
data
dari interfaceb
sebagai argumen (penerima metode).
Sumber gambar: research.swtch.com
Proses lookup di itab
inilah yang disebut "dispatch" dan karena terjadi saat runtime, ia disebut dynamic dispatch.
## 3. Topik Lanjutan dan Implikasi
a. Method Sets: Pointer vs. Value Receiver
Ini adalah aspek penting dan terkadang membingungkan di Go. Aturan dasarnya adalah:
Jika metode didefinisikan dengan value receiver
func (t Tipe) NamaMetode()
, baik nilai bertipeTipe
maupun pointer*Tipe
dapat memanggil metode tersebut dan memenuhi interface.Jika metode didefinisikan dengan pointer receiver
func (t *Tipe) NamaMetode()
, hanya pointer*Tipe
yang memenuhi interface tersebut. NilaiTipe
biasa tidak.
Mengapa? Karena Go dapat secara otomatis mengambil alamat dari sebuah value (t
menjadi &t
) untuk memanggil metode pointer, tetapi tidak sebaliknya. Go tidak bisa secara otomatis "melepas" pointer untuk mendapatkan value, karena bisa jadi pointernya nil
.
Contoh:
package main
import "fmt"
type Pengubah interface {
Ubah(nilai int)
}
type Angka struct {
Nilai int
}
// Metode dengan Pointer Receiver
// Hanya *Angka yang akan memenuhi interface Pengubah
func (a *Angka) Ubah(nilai int) {
a.Nilai = nilai
}
func main() {
// var angka1 Angka = Angka{Nilai: 10}
// var p1 Pengubah = angka1 // <-- INI AKAN GAGAL KOMPILASI!
// Error: Angka does not implement Pengubah (Ubah method has pointer receiver)
var angka2 *Angka = &Angka{Nilai: 20}
var p2 Pengubah = angka2 // <-- INI BERHASIL!
p2.Ubah(99)
fmt.Println(angka2.Nilai) // Output: 99
}
Pemahaman method set sangat krusial saat bekerja dengan interface karena menentukan apakah sebuah tipe konkret benar-benar memenuhi kontrak interface atau tidak.
b. Implikasi Kinerja
Static Dispatch: Panggilan fungsi atau metode secara langsung (
persegi.HitungLuas()
) disebut static dispatch. Kompiler tahu persis fungsi mana yang harus dipanggil. Ini sangat cepat.Dynamic Dispatch: Panggilan melalui interface (
b.HitungLuas()
) memiliki sedikit overhead. Ada dua langkah tambahan:Membaca pointer
itab
dari interface.Mencari pointer fungsi yang benar di dalam
itab
.
Meskipun ada overhead, ini sangat kecil dan dioptimalkan oleh Go. Dalam 99% kasus, dampak kinerjanya dapat diabaikan dibandingkan dengan fleksibilitas luar biasa yang diberikannya. Anda hanya perlu khawatir tentang ini di kode yang sangat kritis terhadap kinerja (misalnya, di dalam loop yang berjalan jutaan kali per detik).
c. Jebakan: nil
Interface vs. Interface dengan Nilai nil
Ini adalah jebakan klasik di Go yang berhubungan langsung dengan struktur internal interface.
Sebuah interface bernilai
nil
hanya jika kedua pointernya (tipe
dandata
) adalahnil
.Jika sebuah interface menampung pointer konkret yang nil, interface itu sendiri TIDAK
nil
.
Contoh:
package main
import "fmt"
type MyError struct{}
func (e *MyError) Error() string {
return "ini error saya"
}
func GetError() *MyError {
// Fungsi ini mengembalikan pointer nil, tetapi bertipe *MyError
return nil
}
func main() {
// Kasus 1: Interface benar-benar nil
var err1 error
fmt.Printf("err1: (Tipe: %T, Nilai: %v), Apakah nil? %v\n", err1, err1, err1 == nil)
// Output: err1: (Tipe: <nil>, Nilai: <nil>), Apakah nil? true
fmt.Println("---")
// Kasus 2: Interface menampung pointer yang nil
var err2 error = GetError() // GetError() mengembalikan (*MyError)(nil)
fmt.Printf("err2: (Tipe: %T, Nilai: %v), Apakah nil? %v\n", err2, err2, err2 == nil)
// Output: err2: (Tipe: *main.MyError, Nilai: <nil>), Apakah nil? false
// Ini sering menyebabkan bug:
if err2 != nil {
fmt.Println("Oops, program mendeteksi error padahal pointer-nya nil!")
// Kode ini akan dieksekusi karena err2 tidak nil.
// Interface err2 punya Tipe (*main.MyError) tapi Datanya nil.
}
}
Ini terjadi karena err2
memiliki pointer tipe yang menunjuk ke *main.MyError
, meskipun pointer data-nya nil
. Karena salah satu pointernya tidak nil
, maka err2
secara keseluruhan tidak dianggap nil
.
Ringkasan
Definisi
Mekanisme untuk menentukan metode mana yang akan dieksekusi pada saat runtime.
Mekanisme di Go
Diimplementasikan melalui interfaces.
Cara Kerja
Variabel interface berisi 2 pointer: tipe konkret dan data konkret. Panggilan metode dilakukan melalui lookup table (itab
) yang ditentukan oleh tipe konkret.
Kelebihan
Memungkinkan kode yang fleksibel, decoupled (tidak saling terikat), dan mendukung polimorfisme.
Kekurangan
Memiliki overhead kinerja yang sangat kecil dibandingkan pemanggilan langsung (static dispatch).
Poin Penting
Pahami Method Sets (perbedaan value vs. pointer receiver) dan waspadai jebakan interface nil
.
Last updated