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)— mengembalikanreflect.Type, yang berisi informasi tentang tipe dari nilaivreflect.ValueOf(v)— mengembalikanreflect.Value, yang berisi nilai aktual darivbeserta 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:
| Kriteria | Reflection | Pendekatan Biasa |
|---|---|---|
| Kecepatan eksekusi | Lebih lambat (3–10x) | Native, optimal |
| Keamanan tipe | Runtime (panic jika salah) | Compile-time |
| Kemudahan baca | Lebih kompleks | Langsung jelas |
| Fleksibilitas | Sangat tinggi | Terbatas 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.