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:

  1. Tipe: Pointer ke informasi tipe konkret yang disimpan (misalnya, *main.Persegi).

  2. 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:

  1. Go melihat ke dalam variabel interface b.

  2. Ia mengikuti pointer tab untuk menemukan itab yang sesuai.

  3. Di dalam itab, ia mencari entri untuk metode HitungLuas().

  4. Entri ini berisi alamat (pointer) dari fungsi Persegi.HitungLuas() (atau Lingkaran.HitungLuas()).

  5. Go kemudian memanggil fungsi tersebut, dengan memberikan pointer data dari interface b 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 bertipe Tipe 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. Nilai Tipe 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:

    1. Membaca pointer itab dari interface.

    2. 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 dan data) adalah nil.

  • 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

Aspek
Penjelasan

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