Banyak fungsi generik hanya butuh satu kemampuan dari tipe penggantinya: membandingkan kesamaan nilai dengan ==. Mencari posisi elemen, menghapus duplikat, dan menghitung frekuensi kemunculan semuanya berdiri di atas operator ==. Go menyediakan constraint predeclared comparable untuk kebutuhan tepat ini, sekaligus menjelaskan mengapa kunci map wajib comparable. Chapter ini membedah aturan tipe mana saja yang comparable, memakainya sebagai type parameter, dan membuktikan batasnya: slice, map, dan function tidak pernah comparable.

comparable Adalah Constraint Predeclared #

comparable adalah nama bawaan (predeclared) yang dideklarasikan sebagai interface dengan himpunan tipe semua nilai yang bisa dibandingkan dengan == dan !=. Constraint ini dipakai langsung pada deklarasi type parameter tanpa perlu import apa pun:

go
func Index[T comparable](data []T, dicari T) int {
	for i, v := range data {
		if v == dicari {
			return i
		}
	}
	return -1
}

Di dalam fungsi, operator == sah dipakai pada v dan dicari karena constraint menjamin setiap tipe pengganti T mendukung operator tersebut. Satu hal yang sering keliru dipahami: comparable hanya menjamin kesamaan (==, !=), bukan pengurutan. Tipe string bisa diurutkan dengan <, tetapi struct umumnya tidak; constraint untuk pengurutan adalah cmp.Ordered yang kita singgung pada chapter Type Constraint .

Syarat Kunci Map #

Map di Go menyimpan pasangan kunci-nilai dalam struktur hash, dan struktur hash hanya bisa bekerja bila setiap kunci mendukung ==. Itulah sebabnya spesifikasi Go mensyaratkan kunci map bertipe comparable. Constraint comparable mengangkat aturan yang sama ke fungsi generik: type parameter yang dipakai sebagai kunci map harus dibatasi comparable, sebagaimana fungsi Frekuensi pada program demo kita nanti yang membangun map[T]int dari T.

Tipe Mana yang Comparable #

Aturan comparability mengikuti definisi spesifikasi Go, dan tabel berikut merangkumnya:

TipeComparableDasar perbandingan
bool, numerik, stringYaNilai isinya
Pointer, channelYaIdentitas (alamat/tujuan sama)
ArrayYa, jika elemen comparableElemen demi elemen
StructYa, jika semua field comparableField demi field
Slice, map, functionTidakHanya bisa dibandingkan dengan nil

Dua baris terakhir tabel butuh penekanan. Slice, map, dan function berisi referensi ke struktur internal yang tidak punya definisi kesamaan nilai; perbandingan x == y pada ketiganya ditolak compiler, dan perbandingan x == nil tetap sah untuk menguji keadaan kosong. Struct menjadi comparable hanya bila seluruh fieldnya comparable — struct berisi satu field slice saja sudah kehilangan status comparable.

Program Demo #

Berkas comparable-demo.go menjalankan fungsi Index untuk int, string, dan struct, lalu memakai comparable sebagai kunci map:

go
package main

import "fmt"

type Peserta struct {
	Nama string
	Skor int
}

// Index mengembalikan posisi pertama nilai yang sama, atau -1.
func Index[T comparable](data []T, dicari T) int {
	for i, v := range data {
		if v == dicari {
			return i
		}
	}
	return -1
}

// Frekuensi menghitung kemunculan tiap nilai; T dipakai sebagai kunci map.
func Frekuensi[T comparable](data []T) map[T]int {
	hasil := make(map[T]int)
	for _, v := range data {
		hasil[v]++
	}
	return hasil
}

func main() {
	angka := []int{10, 20, 30, 40}
	fmt.Println("Index angka 30  :", Index(angka, 30))
	fmt.Println("Index angka 99  :", Index(angka, 99))

	buah := []string{"apel", "jeruk", "mangga"}
	fmt.Println("Index pisang    :", Index(buah, "pisang"))

	// Struct comparable karena seluruh fieldnya comparable.
	daftar := []Peserta{
		{Nama: "Budi", Skor: 88},
		{Nama: "Siti", Skor: 92},
	}
	fmt.Println("Index Siti      :", Index(daftar, Peserta{Nama: "Siti", Skor: 92}))

	// T yang comparable sah dipakai sebagai kunci map.
	suara := Frekuensi([]string{"ya", "tidak", "ya", "ya", "tidak"})
	fmt.Println("Frekuensi suara:", suara)
}

Output yang Dihasilkan:

text
Index angka 30  : 2
Index angka 99  : -1
Index pisang    : -1
Index Siti      : 1
Frekuensi suara: map[tidak:2 ya:3]

Mari kita telaah baris demi baris program di atas:

  1. Baris 5-8 (type Peserta): struct dengan dua field comparable (string dan int) membuat Peserta comparable secara keseluruhan.
  2. Baris 11 (func Index[T comparable]): constraint comparable mengizinkan operator == pada baris 13; fungsi ini kini bekerja untuk semua tipe comparable tanpa duplikasi.
  3. Baris 21 (func Frekuensi[T comparable]): type parameter T dipakai sebagai kunci map[T]int pada baris 22 — pemakaian ini sah justru karena constraint menjamin syarat kunci map.
  4. Baris 31-32: pemanggilan untuk []int mengembalikan posisi 30 yaitu indeks 2; nilai 99 tidak ditemukan sehingga nilai baliknya -1.
  5. Baris 42: pembandingan dua struct dilakukan field demi field; literal Peserta{Nama: "Siti", Skor: 92} sama nilainya dengan elemen indeks 1 sehingga hasilnya 1.
  6. Baris 45-46 (Frekuensi): string "ya" muncul tiga kali dan "tidak" dua kali; fmt.Println mengurutkan kunci map sejak Go 1.12 sehingga output map[tidak:2 ya:3] selalu terurut dan deterministik antar eksekusi.

Batas comparable: Slice Bukan Tipe Comparable #

Fungsi Index di atas menolak slice sebagai argumen bukan karena aturan generics, melainkan karena slice memang tidak comparable. Berkas error-slice.go membuktikan batas tersebut:

go
package main

import "fmt"

// Index mengembalikan posisi pertama nilai yang sama, atau -1.
func Index[T comparable](data []T, dicari T) int {
	for i, v := range data {
		if v == dicari {
			return i
		}
	}
	return -1
}

func main() {
	// Slice tidak comparable sehingga []int tidak memenuhi comparable.
	matriks := [][]int{{1, 2}, {3, 4}}
	fmt.Println(Index(matriks, []int{3, 4}))
}

Output yang Dihasilkan:

text
# command-line-arguments
./error-slice.go:18:19: []int does not satisfy comparable

Compiler menolak [][]int pada saat kompilasi karena tipe elemen []int tidak termasuk himpunan comparable. Pesan yang sama muncul untuk map dan function. Ketika Anda membutuhkan kesamaan isi dua slice, gunakan slices.Equal dari package slices — fungsi generik resmi yang membandingkan elemen satu per satu.

Referensi #

comparable bekerja karena compiler tahu isi himpunan tipe di baliknya — mekanisme deklaratif yang bernama type sets. Chapter Type Sets membuka kotak hitam tersebut: bagaimana union, tilde, dan irisan interface menyusun himpunan tipe yang menentukan operasi sah pada type parameter.

Berkas Sumber

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

comparable-demo.go

Unduh berkas
go
package main

import "fmt"

type Peserta struct {
	Nama string
	Skor int
}

// Index mengembalikan posisi pertama nilai yang sama, atau -1.
func Index[T comparable](data []T, dicari T) int {
	for i, v := range data {
		if v == dicari {
			return i
		}
	}
	return -1
}

// Frekuensi menghitung kemunculan tiap nilai; T dipakai sebagai kunci map.
func Frekuensi[T comparable](data []T) map[T]int {
	hasil := make(map[T]int)
	for _, v := range data {
		hasil[v]++
	}
	return hasil
}

func main() {
	angka := []int{10, 20, 30, 40}
	fmt.Println("Index angka 30  :", Index(angka, 30))
	fmt.Println("Index angka 99  :", Index(angka, 99))

	buah := []string{"apel", "jeruk", "mangga"}
	fmt.Println("Index pisang    :", Index(buah, "pisang"))

	// Struct comparable karena seluruh fieldnya comparable.
	daftar := []Peserta{
		{Nama: "Budi", Skor: 88},
		{Nama: "Siti", Skor: 92},
	}
	fmt.Println("Index Siti      :", Index(daftar, Peserta{Nama: "Siti", Skor: 92}))

	// T yang comparable sah dipakai sebagai kunci map.
	suara := Frekuensi([]string{"ya", "tidak", "ya", "ya", "tidak"})
	fmt.Println("Frekuensi suara:", suara)
}

error-slice.go

Unduh berkas
go
package main

import "fmt"

// Index mengembalikan posisi pertama nilai yang sama, atau -1.
func Index[T comparable](data []T, dicari T) int {
	for i, v := range data {
		if v == dicari {
			return i
		}
	}
	return -1
}

func main() {
	// Slice tidak comparable sehingga []int tidak memenuhi comparable.
	matriks := [][]int{{1, 2}, {3, 4}}
	fmt.Println(Index(matriks, []int{3, 4}))
}