Dari semua kejutan yang disiapkan Go untuk pemrogram baru, satu ini menempati peringkat teratas: sebuah interface bisa tidak sama dengan nil meskipun nilainya pointer nil. Bug yang lahir dari perilaku ini biasanya muncul di jalur error handling — fungsi mengembalikan “tidak ada galat”, pemanggil memeriksanya, dan pemeriksaan err != nil tetap mengatakan ada galat. FAQ resmi Go bahkan menyediakan bagian khusus untuk menjelaskan kasus ini. Chapter ini membongkar akar masalahnya lewat model pasangan (dynamic type, dynamic value), mendemonstrasikan jebakannya dengan program nyata, lalu memberikan pola penutup yang aman.

Interface nil Sejati #

Variabel interface yang belum diisi memiliki zero value berupa pasangan kosong: dynamic type kosong dan dynamic value kosong. Dalam keadaan ini, perbandingan i == nil menghasilkan true — dynamic type tidak ada, jadi tidak ada apa pun untuk dibandingkan. Namun memanggil method pada interface seperti ini berakhir panic, karena runtime tidak menemukan dynamic type untuk mencari method:

text
panic: runtime error: invalid memory address or nil pointer dereference

Pesan di atas adalah baris pertama hasil eksekusi nyata pemanggilan method pada interface nil; sisa keluarannya berupa stack trace dengan alamat memori. Interface nil boleh diperiksa dan dilewatkan, tetapi tidak boleh dipakai memanggil method.

Pointer nil di Dalam Interface #

Masalah mulai muncul ketika pointer nil dibungkus ke dalam interface. Penugasan var i Alat = sensor dengan sensor bertipe *Sensor bernilai nil tidak menghasilkan interface nil. Penugasan tersebut mengisi pasangan: dynamic type *Sensor tetap tercatat, dynamic value-nya nil. Perbandingan i == nil kini membandingkan pasangan lengkap terhadap pasangan kosong — hasilnya false.

Perilaku ini bukan cacat desain. Dynamic type harus tersimpan supaya runtime tahu method mana yang dipanggil, dan di Go pointer nil yang bertipe tetap sah sebagai receiver: method Status milik *Sensor di demo di bawah sengaja memeriksa s == nil dan tetap mengembalikan teks yang bermakna. Interface yang berisi (*Sensor, nil) masih bisa memanggil method itu dengan aman; interface nil sejati tidak bisa memanggil apa pun.

Jebakan pada Pola Return Error #

Bentuk nyata bug ini hampir selalu melibatkan error. Polanya: fungsi helper dideklarasikan mengembalikan tipe pointer konkret — misalnya *GalatPencarian — dan mengembalikan nil saat tidak ada masalah. Pemanggil lalu menugaskan hasilnya ke variabel bertipe error (yang adalah interface). Penugasan itu membentuk pasangan (*GalatPencarian, nil), sehingga err != nil bernilai true padahal logika bisnisnya bilang tidak ada galat. Demo pada program di bawah memperlihatkan kedua sisi sekaligus: galat == nil bernilai true, err == nil bernilai false, padahal keduanya berasal dari nilai yang sama.

Solusi: Kembalikan nil Literal atau Tipe Konkret #

Dua penutup yang aman: pertama, deklarasikan fungsi dengan tipe hasil error dan kembalikan nil literal — return nil pada tipe hasil interface menghasilkan pasangan kosong, bukan pasangan bertipe. Kedua, pertahankan tipe hasil konkret *GalatPencarian lalu biarkan pemanggil membandingkan nilainya sebelum dibungkus ke interface. Bentuk pertama pada umumnya lebih disukai karena pemanggil cukup memeriksa err != nil:

go
func cariPengguna(nama string) error {
	if nama == "" {
		return &GalatPencarian{Pesan: "nama kosong"}
	}
	return nil // nil literal: pasangan benar-benar kosong
}

Program Demo #

Berkas nil-demo.go memperagakan ketiga keadaan interface dalam satu program:

go
package main

import "fmt"

// Alat adalah interface dengan satu method.
type Alat interface {
	Status() string
}

// Sensor mengimplementasikan Alat lewat receiver pointer.
type Sensor struct {
	Nama string
}

func (s *Sensor) Status() string {
	if s == nil {
		return "sensor tidak terpasang"
	}
	return s.Nama + " aktif"
}

// GalatPencarian adalah tipe error konkret dengan receiver pointer.
type GalatPencarian struct {
	Pesan string
}

func (g *GalatPencarian) Error() string {
	return g.Pesan
}

// cariPengguna mengembalikan pointer nil saat pengguna ditemukan tanpa masalah.
func cariPengguna(nama string) *GalatPencarian {
	if nama == "" {
		return &GalatPencarian{Pesan: "nama kosong"}
	}
	return nil
}

func main() {
	// Kasus 1: interface nil sejati, tanpa dynamic type.
	var alatKosong Alat
	fmt.Println("alatKosong == nil:", alatKosong == nil)

	// Kasus 2: pointer nil dibungkus interface, dynamic type tetap ada.
	var sensor *Sensor = nil
	var alatTerisi Alat = sensor
	fmt.Println("alatTerisi == nil:", alatTerisi == nil)
	fmt.Println("alatTerisi.Status():", alatTerisi.Status())

	// Kasus 3: pola error yang paling sering menjadi bug di lapangan.
	galat := cariPengguna("Siti")
	fmt.Println("galat == nil:", galat == nil)
	var err error = galat
	fmt.Println("err == nil:", err == nil)
}

Output yang Dihasilkan:

text
alatKosong == nil: true
alatTerisi == nil: false
alatTerisi.Status(): sensor tidak terpasang
galat == nil: true
err == nil: false

Mari kita telaah baris demi baris program di atas:

  1. Baris 6–8 (type Alat interface): kontrak satu method yang menjadi wadah ketiga keadaan interface di program ini.
  2. Baris 15–20 (func (s *Sensor) Status): method menerima receiver pointer dan membuka jalan bagi receiver nil; pemeriksaan s == nil mengembalikan teks yang jelas, bukan panic.
  3. Baris 27–29 (func (g *GalatPencarian) Error): method Error menjadikan pointer *GalatPencarian memenuhi interface error.
  4. Baris 31–37 (func cariPengguna): tipe hasilnya pointer konkret; jalur sukses mengembalikan nil pointer — inilah sumber jebakan pada pemanggil.
  5. Baris 41–42 (var alatKosong Alat): interface tanpa dynamic type; perbandingan dengan nil menghasilkan true.
  6. Baris 45–48 (alatTerisi): pointer nil dibungkus interface; pasangan (*Sensor, nil) membuat perbandingan false, namun pemanggilan Status() tetap aman karena receiver nil ditangani methodnya.
  7. Baris 51–54 (galat dan err): nilai kembalian yang sama diperiksa dua cara — sebagai pointer konkret hasilnya true, sebagai interface error hasilnya false; baris terakhir itulah wujud bug yang paling dicari pemrogram Go.

Paham pasangan (dynamic type, dynamic value) membuat seluruh gejala di atas bisa diprediksi, bukan dihafal. Dengan jebakan nil terlewati, kita siap melangkah ke sisi konstruktif interface: menggabungkan kontrak kecil menjadi kontrak yang lebih besar lewat komposisi.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

nil-demo.go

Unduh berkas
go
package main

import "fmt"

// Alat adalah interface dengan satu method.
type Alat interface {
	Status() string
}

// Sensor mengimplementasikan Alat lewat receiver pointer.
type Sensor struct {
	Nama string
}

func (s *Sensor) Status() string {
	if s == nil {
		return "sensor tidak terpasang"
	}
	return s.Nama + " aktif"
}

// GalatPencarian adalah tipe error konkret dengan receiver pointer.
type GalatPencarian struct {
	Pesan string
}

func (g *GalatPencarian) Error() string {
	return g.Pesan
}

// cariPengguna mengembalikan pointer nil saat pengguna ditemukan tanpa masalah.
func cariPengguna(nama string) *GalatPencarian {
	if nama == "" {
		return &GalatPencarian{Pesan: "nama kosong"}
	}
	return nil
}

func main() {
	// Kasus 1: interface nil sejati, tanpa dynamic type.
	var alatKosong Alat
	fmt.Println("alatKosong == nil:", alatKosong == nil)

	// Kasus 2: pointer nil dibungkus interface, dynamic type tetap ada.
	var sensor *Sensor = nil
	var alatTerisi Alat = sensor
	fmt.Println("alatTerisi == nil:", alatTerisi == nil)
	fmt.Println("alatTerisi.Status():", alatTerisi.Status())

	// Kasus 3: pola error yang paling sering menjadi bug di lapangan.
	galat := cariPengguna("Siti")
	fmt.Println("galat == nil:", galat == nil)
	var err error = galat
	fmt.Println("err == nil:", err == nil)
}