BAB 21: Reflect — Inspeksi Tipe di Runtime

Pahami cara kerja package reflect untuk menginspeksi tipe dan nilai secara dinamis saat program berjalan.

Di bab sebelumnya, interface mengajarkan kamu cara menulis kode yang tidak bergantung pada tipe konkret — kamu mendefinisikan kontrak, dan siapa pun yang memenuhi kontrak itu bisa masuk. Tapi ada satu pertanyaan yang interface tidak bisa jawab: bagaimana kalau kamu ingin tahu tipe konkret itu apa, di saat program sudah berjalan? Bagaimana kalau kamu ingin mengiterasi semua field sebuah struct tanpa mengetahui nama fieldnya lebih dulu?

Di sinilah reflect hadir — kemampuan Go untuk menginspeksi dan memanipulasi nilai secara dinamis saat runtime.

Apa Itu Reflection?

Reflection adalah mekanisme yang memungkinkan program memeriksa struktur dirinya sendiri saat berjalan. Bukan saat dikompilasi, tapi saat program sudah hidup dan bereksekusi. Go menyediakan ini melalui package standar reflect.

Analoginya seperti cermin: saat kamu berdiri di depan cermin, kamu bisa melihat sendiri seperti apa tampilanmu — tinggi, warna baju, posisi tangan. Reflection memberikan kemampuan serupa kepada kode: program bisa “melihat” dirinya sendiri — tipe apa variabel ini, field apa saja yang ada di struct ini, method apa yang bisa dipanggil.

Dua fungsi utama yang akan selalu dipakai:

  • reflect.TypeOf(v) — mengembalikan reflect.Type, yang berisi informasi tentang tipe dari nilai v
  • reflect.ValueOf(v) — mengembalikan reflect.Value, yang berisi nilai aktual dari v beserta metadata-nya

TypeOf dan ValueOf

Mari mulai dengan contoh paling sederhana: menginspeksi variabel biasa.

// main.go
package main

import (
    "fmt"
    "reflect"
)

func main() {
    stok := 150
    harga := 29500.50
    nama := "Buku Go Dasar"

    fmt.Println("--- stok ---")
    fmt.Println("Tipe:", reflect.TypeOf(stok))
    fmt.Println("Nilai:", reflect.ValueOf(stok))

    fmt.Println("--- harga ---")
    fmt.Println("Tipe:", reflect.TypeOf(harga))
    fmt.Println("Nilai:", reflect.ValueOf(harga))

    fmt.Println("--- nama ---")
    fmt.Println("Tipe:", reflect.TypeOf(nama))
    fmt.Println("Nilai:", reflect.ValueOf(nama))
}
--- stok ---
Tipe: int
Nilai: 150
--- harga ---
Tipe: float64
Nilai: 29500.5
--- nama ---
Tipe: string
Nilai: Buku Go Dasar

reflect.TypeOf mengembalikan nama tipe sebagai string — int, float64, string. Sedangkan reflect.ValueOf mengembalikan objek reflect.Value yang membungkus nilai aslinya.

Kind — Kategori Tipe

Selain tipe, setiap nilai punya Kind — kategori lebih general dari tipe. Misalnya, tipe int, int32, dan int64 semuanya termasuk kategori reflect.Int (atau variannya). Begitu juga struct yang berbeda-beda — semuanya berkategori reflect.Struct.

// main.go
package main

import (
    "fmt"
    "reflect"
)

type Produk struct {
    Nama  string
    Stok  int
}

func main() {
    p := Produk{Nama: "Kopi Arabika", Stok: 80}
    angka := 42
    teks := "halo"

    fmt.Println(reflect.TypeOf(p).Kind())      // struct
    fmt.Println(reflect.TypeOf(angka).Kind())  // int
    fmt.Println(reflect.TypeOf(teks).Kind())   // string
    fmt.Println(reflect.TypeOf(&p).Kind())     // ptr
    fmt.Println(reflect.TypeOf([]int{}).Kind()) // slice
}
struct
int
string
ptr
slice

Kind berguna saat kamu menulis function yang menerima any dan perlu berperilaku berbeda tergantung jenis datanya — tanpa harus tahu nama tipe persisnya.

Mengakses Value Berdasarkan Kind

Setelah mendapatkan reflect.Value, kamu bisa mengekstrak nilai aslinya dengan method yang sesuai dengan kind-nya.

// main.go
package main

import (
    "fmt"
    "reflect"
)

func cetakNilai(v any) {
    rv := reflect.ValueOf(v)

    switch rv.Kind() {
    case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
        fmt.Printf("int: %d\n", rv.Int())
    case reflect.Float32, reflect.Float64:
        fmt.Printf("float: %.2f\n", rv.Float())
    case reflect.String:
        fmt.Printf("string: %q (panjang: %d)\n", rv.String(), rv.Len())
    case reflect.Bool:
        fmt.Printf("bool: %v\n", rv.Bool())
    default:
        fmt.Printf("tipe lain: %s\n", rv.Kind())
    }
}

func main() {
    cetakNilai(512)
    cetakNilai(99.95)
    cetakNilai("stok habis")
    cetakNilai(true)
    cetakNilai([]string{"a", "b"})
}
int: 512
float: 99.95
string: "stok habis" (panjang: 10)
bool: true
tipe lain: slice

Perhatikan bahwa rv.Int() selalu mengembalikan int64 — bukan int. Begitu juga rv.Float() mengembalikan float64. Ini konsisten untuk semua varian integer dan float.

Method seperti rv.Int(), rv.Float(), dan rv.String() akan panic jika dipanggil pada Value dengan kind yang tidak sesuai. Selalu cek Kind() lebih dulu sebelum memanggil method tersebut.

Mengiterasi Field Struct

Salah satu kekuatan reflect yang paling sering dimanfaatkan adalah mengiterasi semua field dalam struct — berguna untuk membuat serializer, validator, atau logger generik.

// main.go
package main

import (
    "fmt"
    "reflect"
)

type Inventaris struct {
    KodeBarang string
    NamaBarang string
    Stok       int
    Harga      float64
    Aktif      bool
}

func inspeksiStruct(v any) {
    rv := reflect.ValueOf(v)
    rt := reflect.TypeOf(v)

    // pastikan yang masuk adalah struct
    if rv.Kind() != reflect.Struct {
        fmt.Println("bukan struct")
        return
    }

    fmt.Printf("Struct: %s\n", rt.Name())
    fmt.Println("Fields:")

    for i := 0; i < rv.NumField(); i++ {
        field := rt.Field(i)
        value := rv.Field(i)

        fmt.Printf("  %-14s | %-8s | %v\n", field.Name, field.Type, value)
    }
}

func main() {
    item := Inventaris{
        KodeBarang: "PRD-001",
        NamaBarang: "Mechanical Keyboard",
        Stok:       45,
        Harga:      850000,
        Aktif:      true,
    }

    inspeksiStruct(item)
}
Struct: Inventaris
Fields:
  KodeBarang     | string   | PRD-001
  NamaBarang     | string   | Mechanical Keyboard
  Stok           | int      | 45
  Harga          | float64  | 850000
  Aktif          | bool     | true

rt.NumField() mengembalikan jumlah field, rt.Field(i) mengembalikan reflect.StructField yang berisi nama dan tipe, sedangkan rv.Field(i) mengembalikan reflect.Value yang berisi nilainya.

Membaca Struct Tag

Struct tag sering dipakai oleh library seperti encoding/json dan gorm untuk instruksi tambahan. Dengan reflect, kamu bisa membacanya:

// main.go
package main

import (
    "fmt"
    "reflect"
)

type Transaksi struct {
    ID        int     `db:"id" json:"id"`
    Jumlah    float64 `db:"jumlah" json:"jumlah"`
    Keterangan string `db:"keterangan" json:"keterangan,omitempty"`
}

func bacaTag(v any) {
    rt := reflect.TypeOf(v)

    if rt.Kind() != reflect.Struct {
        return
    }

    for i := 0; i < rt.NumField(); i++ {
        field := rt.Field(i)
        dbTag := field.Tag.Get("db")
        jsonTag := field.Tag.Get("json")
        fmt.Printf("Field: %-12s | db: %-14s | json: %s\n", field.Name, dbTag, jsonTag)
    }
}

func main() {
    bacaTag(Transaksi{})
}
Field: ID           | db: id             | json: id
Field: Jumlah       | db: jumlah         | json: jumlah
Field: Keterangan   | db: keterangan     | json: keterangan,omitempty

field.Tag.Get("key") mengembalikan nilai tag untuk key tertentu, atau string kosong jika key tidak ada.

Memanggil Method Secara Dinamis

Reflect juga bisa memanggil method dari suatu nilai hanya berdasarkan namanya — tanpa mengetahui tipe konkretnya saat kompilasi.

// main.go
package main

import (
    "fmt"
    "reflect"
)

type Laporan struct {
    Judul  string
    Isi    string
}

func (l Laporan) Ringkas() string {
    if len(l.Isi) > 30 {
        return l.Judul + ": " + l.Isi[:30] + "..."
    }
    return l.Judul + ": " + l.Isi
}

func (l Laporan) Panjang() int {
    return len(l.Isi)
}

func panggilMethod(v any, namaMethod string) {
    rv := reflect.ValueOf(v)
    method := rv.MethodByName(namaMethod)

    if !method.IsValid() {
        fmt.Printf("method %q tidak ditemukan\n", namaMethod)
        return
    }

    hasil := method.Call(nil)
    for _, h := range hasil {
        fmt.Printf("Hasil %s(): %v\n", namaMethod, h)
    }
}

func main() {
    lap := Laporan{
        Judul: "Penjualan Q1",
        Isi:   "Total penjualan meningkat 23% dibanding kuartal sebelumnya.",
    }

    panggilMethod(lap, "Ringkas")
    panggilMethod(lap, "Panjang")
    panggilMethod(lap, "Ekspor")
}
Hasil Ringkas(): Penjualan Q1: Total penjualan meningkat 23...
Hasil Panjang(): 56
method "Ekspor" tidak ditemukan

MethodByName mengembalikan reflect.Value yang merepresentasikan method. IsValid() perlu dicek karena jika method tidak ada, MethodByName mengembalikan zero Value. Call(nil) memanggil method tanpa argumen dan mengembalikan slice reflect.Value berisi semua return value.

Method yang bisa diakses lewat reflect hanyalah method yang di-export (huruf kapital). Method unexported tidak bisa dipanggil via reflection dari luar package.

Mengubah Nilai via Reflection

Reflect tidak hanya untuk membaca — kamu juga bisa mengubah nilai, tapi dengan satu syarat: nilai harus bisa di-address, artinya harus dikirim sebagai pointer.

// main.go
package main

import (
    "fmt"
    "reflect"
)

type Konfigurasi struct {
    Host    string
    Port    int
    Debug   bool
}

func terapkanDefault(ptr any) {
    rv := reflect.ValueOf(ptr)

    // harus pointer ke struct
    if rv.Kind() != reflect.Ptr || rv.Elem().Kind() != reflect.Struct {
        fmt.Println("butuh pointer ke struct")
        return
    }

    elem := rv.Elem()
    rt := elem.Type()

    for i := 0; i < elem.NumField(); i++ {
        field := elem.Field(i)
        fieldType := rt.Field(i)

        // isi hanya field yang masih zero value
        if field.IsZero() && field.CanSet() {
            switch field.Kind() {
            case reflect.String:
                field.SetString("(tidak diatur)")
            case reflect.Int:
                field.SetInt(0)
            case reflect.Bool:
                field.SetBool(false)
            }
            fmt.Printf("Field %s diisi dengan default\n", fieldType.Name)
        }
    }
}

func main() {
    cfg := Konfigurasi{
        Host: "localhost",
    }

    fmt.Println("Sebelum:", cfg)
    terapkanDefault(&cfg)
    fmt.Println("Sesudah:", cfg)
}
Field Port diisi dengan default
Field Debug diisi dengan default
Sebelum: {localhost 0 false}
Sesudah: {localhost 0 false}

rv.Elem() mengikuti pointer dan mengembalikan nilai yang ditunjuk. field.CanSet() memastikan field bisa diubah — field harus exported dan nilainya harus addressable. field.IsZero() mengecek apakah nilai masih dalam kondisi zero value untuk tipenya.

Kapan Pakai Reflect, Kapan Tidak

Reflect adalah alat yang kuat, tapi ada harga yang harus dibayar. Berikut perbandingan yang perlu dipahami sebelum memutuskan menggunakannya:

KriteriaReflectionPendekatan Biasa
Kecepatan eksekusiLebih lambat (3–10x)Native, optimal
Keamanan tipeRuntime (panic jika salah)Compile-time
Kemudahan bacaLebih kompleksLangsung jelas
FleksibilitasSangat tinggiTerbatas pada tipe yang diketahui

Gunakan reflect untuk:

  • Serialisasi dan deserialisasi generik (JSON encoder, XML parser)
  • Framework testing yang perlu membandingkan struct sembarang
  • Dependency injection dan container
  • Tool seperti ORM yang perlu membaca struct tag

Hindari reflect untuk:

  • Logic bisnis biasa — buat function dengan tipe konkret
  • Hot path yang dipanggil jutaan kali — overhead-nya terasa
  • Kode yang bisa diselesaikan dengan generics (Go 1.18+)

Jika kamu butuh generic behavior, coba generics dulu sebelum reflect. Generics memberikan fleksibilitas tipe tanpa overhead runtime dan tetap aman di compile-time.

Latihan

Latihan 1 — Inspeksi any: Buat function deskripsikan(v any) yang menerima nilai sembarang dan mencetak: tipe, kind, dan nilainya. Untuk struct, cetak juga jumlah field dan nama masing-masing field.

Latihan 2 — Konverter struct ke map: Buat function strukturKeMap(v any) map[string]any yang menerima struct dan mengembalikan map dengan key nama field dan value-nya. Gunakan reflect untuk mengiterasi field. Pastikan hanya memproses field yang exported.

Latihan 3 — Validator generik: Buat function validasiWajibIsi(ptr any) []string yang menerima pointer ke struct dan mengembalikan slice berisi nama field yang masih zero value. Gunakan reflect untuk mengiterasi dan mengecek tiap field. Uji dengan struct yang beberapa fieldnya kosong dan beberapa lagi sudah terisi.

Reflect membuka pintu ke pola pemrograman yang tidak mungkin dilakukan tanpa inspeksi runtime — dari serializer generik sampai framework yang bisa beradaptasi dengan tipe apa pun. Dengan memahami cara kerjanya, kamu juga akan lebih mudah memahami library Go populer yang menggunakannya secara intensif, seperti encoding/json, database/sql, dan berbagai ORM.

Selama ini semua kode yang kita tulis berjalan secara sekuensial — satu baris selesai, baru baris berikutnya dieksekusi. Tapi Go sejak awal dirancang untuk lebih dari itu. Kemampuan menjalankan banyak pekerjaan secara bersamaan adalah salah satu alasan Go dipilih untuk sistem seperti Docker dan Kubernetes. Di bab selanjutnya, kita akan masuk ke dunia concurrency — dimulai dari goroutine, unit terkecil dari eksekusi concurrent di Go.

Referensi

  1. 1reflect — Go Packages (dokumentasi resmi)
  2. 2The Laws of Reflection — The Go Blog
  3. 3Reflection in Go — GolangBot