Interface Kosong dan any

Kapan pengetikan dinamis beralasan dan perbandingannya dengan generics.

Daftar isi

Interface kosong menampung nilai apa pun karena tidak menuntut satu metode pun. Topik ini membahas deklarasi any, kondisi yang membuat pengetikan dinamis beralasan, serta perbandingannya dengan generics yang memeriksa tipe saat kompilasi. Setelah membacanya, pemilihan antara any, interface kecil, dan parameter tipe dapat dipertimbangkan dengan alasan yang jelas dan bukan sekadar kebiasaan.

Deklarasi any#

Interface kosong (empty interface) adalah interface tanpa metode, sehingga setiap tipe memenuhinya. Deklarasinya memang tidak memiliki isi, dan pustaka standar menyediakannya dengan nama alias any.

go Deklarasi any pada paket builtin
// tidak-dijalankan
type any = interface{}

Alias tersebut diperkenalkan sejak Go 1.18 untuk menuliskan interface{} dengan lebih ringkas, dan keduanya menunjuk tipe yang sama. Variabel any dapat diberi nilai apa pun, tetapi informasi tipe konkretnya tidak tercantum pada tipe statisnya. Program berikut mengisi satu variabel yang sama dengan tiga nilai yang berbeda.

go nilai-apa-pun.go
package main

import "fmt"

func main() {
	var data any

	data = "arsip catatan"
	fmt.Println(data)

	data = 128
	fmt.Println(data)

	data = []string{"draf", "final"}
	fmt.Println(data)

	fmt.Printf("tipe dinamisnya %T\n", data)
}
text Keluaran
arsip catatan
128
[draf final]
tipe dinamisnya []string

Setiap penugasan berhasil karena semua tipe memenuhi any. Sebaliknya, nilai tersebut tidak dapat langsung dipakai sebagai string atau int, karena operasi yang menuntut tipe konkret ditolak saat kompilasi. Tipe dinamisnya dibaca kembali melalui verb %T sebagaimana terlihat pada baris terakhir, dan pemakaiannya kembali memerlukan type assertion atau type switch. Program tersebut dapat dijalankan lewat terminal sebagaimana berikut.

bash
# siapkan folder kerja untuk topik ini
mkdir -p /tmp/gointerface/interface-kosong
cp nilai-apa-pun.go /tmp/gointerface/interface-kosong/
cd /tmp/gointerface/interface-kosong

# periksa format sebelum menjalankan programnya
gofmt -l .

# jalankan programnya
go run nilai-apa-pun.go
text Keluaran
arsip catatan
128
[draf final]
tipe dinamisnya []string

Keluarannya sama dengan blok sebelumnya karena keduanya menjalankan program yang sama, sedangkan gofmt -l tidak mencetak apa pun ketika format kode sudah sesuai.

Kapan pengetikan dinamis beralasan#

Pengetikan dinamis (dynamic typing), yaitu penentuan tipe yang dilakukan saat program berjalan, beralasan pada tiga kondisi. Pertama, pada batas data yang strukturnya ditentukan di luar program, seperti dokumen JSON dan data dari layanan lain. Kedua, pada fungsi yang memang mengelola nilai sembarang, seperti fmt.Println yang parameternya bertipe ...any. Ketiga, pada kontainer yang isinya memang bercampur tipe dan jarang dipakai. Di luar ketiganya, tipe konkret atau interface kecil biasanya memberi jaminan yang lebih kuat.

Paket encoding/json memakai map[string]any untuk data yang strukturnya belum diketahui, dan setiap bilangan pada dokumen menjadi float64. Program berikut membaca dokumen JSON kecil lalu memeriksa tipe tiap nilainya.

go data-json.go
package main

import (
	"encoding/json"
	"fmt"
)

func main() {
	teks := []byte(`{"judul":"log sensor","banyak":24,"final":true}`)

	var data map[string]any
	if err := json.Unmarshal(teks, &data); err != nil {
		fmt.Println("error:", err)
		return
	}

	// kunci diakses lewat daftar tetap agar urutannya pasti
	for _, kunci := range []string{"judul", "banyak", "final"} {
		fmt.Printf("%s: %v (tipe %T)\n", kunci, data[kunci], data[kunci])
	}
}
text Keluaran
judul: log sensor (tipe string)
banyak: 24 (tipe float64)
final: true (tipe bool)

Field banyak yang tertulis 24 terbaca sebagai float64, karena format JSON tidak membedakan bilangan bulat dan bilangan desimal. Pengaksesan kunci dilakukan lewat daftar tetap agar urutan cetakannya pasti, sebab urutan perulangan peta memang tidak dijamin. Untuk data yang strukturnya sudah stabil, deklarasi struct dengan tag JSON menghindarkan kode dari asersi tipe sama sekali.

any dan generics#

Generics (generics), yaitu parameter tipe yang diperiksa saat kompilasi, menyelesaikan persoalan yang berbeda. Fungsi generik menerima satu jenis tipe pada tiap pemanggilannya dan tetap mengenali tipe itu di dalam badan fungsinya, sehingga tidak ada asersi. Program berikut menempatkan keduanya berdampingan pada kasus penjumlahan.

go jumlah-generik.go
package main

import "fmt"

// jumlahAny menerima interface kosong sehingga pemanggilnya dapat mengirim
// nilai apa pun, tetapi setiap pemakaian menuntut asersi tipe.
func jumlahAny(a, b any) (int, error) {
	x, ok := a.(int)
	if !ok {
		return 0, fmt.Errorf("argumen pertama bukan int, melainkan %T", a)
	}
	y, ok := b.(int)
	if !ok {
		return 0, fmt.Errorf("argumen kedua bukan int, melainkan %T", b)
	}
	return x + y, nil
}

// jumlahT adalah fungsi generik: kesalahan tipe terdeteksi saat kompilasi.
func jumlahT[T int | float64](a, b T) T {
	return a + b
}

func main() {
	jumlah, err := jumlahAny(10, 24)
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	fmt.Println("jumlah:", jumlah)

	if _, err := jumlahAny(10, "banyak"); err != nil {
		fmt.Println("error:", err)
	}
	fmt.Println(jumlahT(10, 24))
	fmt.Println(jumlahT(1.5, 2.5))
}
text Keluaran
jumlah: 34
error: argumen kedua bukan int, melainkan string
34
4

Fungsi jumlahAny harus berasersi dua kali dan melaporkan penolakan lewat error saat program berjalan, sedangkan jumlahT menolak pemanggilan yang tipenya tidak cocok lebih dahulu saat kompilasi. Tabel berikut merangkum perbedaan keduanya.

AspekanyGenerics
Pemeriksaan tipeSaat program berjalan, melalui asersiSaat kompilasi
Nilai yang diterimaSembarang tipe, boleh bercampurSatu tipe per instansiasi fungsi
Biaya penulisanAsersi tipe di setiap pemakaianDeklarasi parameter tipe
Pemakaian yang cocokData dinamis seperti JSON dan fungsi pencetakOperasi seragam atas satu tipe

Panduan resmi Go dalam artikel When To Use Generics menempatkan generics sebagai alat untuk kode yang logikanya identik antar tipe dan bukan sebagai pengganti interface. Interface kecil tetap menjadi alat utama abstraksi perilaku, sedangkan any menyatakan bahwa pengetahuan tipe sengaja ditunda.

Kesalahan umum#

Tiga kebiasaan berikut membuat kode yang memakai any menjadi rapuh.

  • Menganggap any bebas aturan tipe. Operasi seperti data + 1 pada variabel any ditolak kompilator dengan pesan invalid operation: data + 1 (mismatched types any and untyped int). Perbaikannya, berasersi lebih dahulu ke tipe konkret, atau gunakan parameter tipe bila operasinya sama untuk banyak tipe.
  • Mengandalkan urutan perulangan peta saat memproses map[string]any. Urutan iterasi peta diacak pada setiap eksekusi, sehingga keluaran yang tercetak berubah-ubah dan perbandingan antar pengujian tidak dapat dipercaya. Perbaikannya, akses kunci lewat daftar tetap atau urutkan kuncinya lebih dahulu.
  • Memilih any demi menghindari penulisan tipe. Asersi pun bermunculan di seluruh kode, dan kesalahan tipe baru terlihat saat program berjalan. Perbaikannya, kembalikan tipe konkret, interface kecil, atau parameter tipe sesuai kebutuhannya.

Ringkasan#

  • any adalah alias untuk interface kosong, dan setiap nilai memenuhinya karena tidak ada metode yang dituntut.
  • Informasi tipe konkret hilang dari tipe statis any, sehingga pemakaiannya kembali menuntut type assertion atau type switch.
  • Pengetikan dinamis beralasan pada batas data eksternal, fungsi yang mengelola nilai sembarang, dan kontainer yang isinya bercampur.
  • Generics memeriksa tipe saat kompilasi dan lebih cocok untuk logika yang identik antar tipe.

Referensi#