Type parameter tanpa batas sama seperti pintu tanpa kunci: semua tipe bisa masuk, termasuk tipe yang tidak mendukung operasi di dalam fungsi. Constraint adalah komponen generics yang menjawab masalah ini — sebuah interface yang menentukan himpunan tipe sah bagi type parameter. Chapter ini membahas bentuk constraint, cara union elemen seperti ~int | ~float64 memperluas himpunan, perbedaan constraint bernama dan constraint literal, serta pilihan constraint siap pakai dari standard library. Program penutup membuktikan constraint bekerja pada tipe dasar maupun tipe buatan sendiri.

Constraint Adalah Interface #

Sejak Go 1.18, interface memikul dua peran: interface biasa menyatakan kumpulan method, sedangkan constraint menyatakan himpunan tipe yang boleh menggantikan type parameter. Constraint dideklarasikan dengan keyword interface seperti biasa:

go
type Number interface {
	~int | ~float64
}

Interface Number di atas tidak memuat method sama sekali; isinya adalah union elemen — daftar tipe yang dipisah tanda |. Fungsi generik yang memakainya hanya menerima tipe anggota himpunannya, dan pemanggilan dengan string menghasilkan pesan does not satisfy sebagaimana kita buktikan pada chapter Motivasi Generics .

Compiler hanya mengizinkan operasi yang sah untuk semua anggota himpunan constraint. Number mengizinkan +, -, *, / dan pembandingan karena seluruh anggotanya mendukung operator tersebut, tetapi operator % tidak diizinkan karena float64 tidak mendukungnya.

Union Elemen dan Tanda Tilde #

Elemen union boleh berupa tipe biasa (int) atau elemen tilde (~int). Perbedaan keduanya menentukan apakah tipe buatan sendiri lolos constraint:

go
type Milimeter int

Elemen int pada union hanya menerima tipe int persis, sedangkan elemen ~int menerima semua tipe yang underlying type-nya int — termasuk Milimeter, Skor, dan tipe bernama lain yang dideklarasikan dari int. Aturan praktisnya: constraint untuk tipe dasar hampir selalu ditulis dengan ~ agar tipe turunan tetap diterima.

Constraint Bernama vs Constraint Literal #

Constraint boleh dipakai lewat nama yang sudah dideklarasikan atau ditulis langsung pada deklarasi fungsi:

go
// Constraint bernama: deklarasi interface dipisah dan bisa dipakai ulang.
func RataRata[T Number](data []T) T { ... }

// Constraint literal: union ditulis langsung di kurung siku.
func KaliDua[T ~int | ~float64](nilai T) T { ... }

Constraint bernama cocok ketika himpunan tipe dipakai berulang kali atau butuh dokumentasi; constraint literal pas untuk fungsi tunggal dengan himpunan sederhana. Keduanya setara secara kemampuan — pilihannya urusan gaya dan pemakaian ulang.

any dan Constraint Bawaan Lainnya #

Go menyediakan dua constraint predeclared. Pertama any — alias interface{} dari chapter Tipe any — menerima semua tipe. Kedua comparable menerima semua tipe yang mendukung operator == dan !=, materi chapter Constraint comparable .

Standard library juga menyediakan constraint siap pakai. Sejak Go 1.21, package cmp memuat cmp.Ordered — union seluruh tipe terurut (semua bilangan bulat, pecahan, dan string) yang mendukung operator < sampai >:

go
func Max[T cmp.Ordered](a, b T) T { ... }

Paket eksternal golang.org/x/exp/constraints yang populer sebelum era cmp kini mendefinisikan Ordered sebagai alias cmp.Ordered; karena bersifat eksternal dan eksperimental, contoh pada course ini memakai standard library saja.

Program Demo #

Berkas constraint-demo.go menguji constraint bernama, constraint literal, dan kekuatan tanda tilde:

go
package main

import "fmt"

// Number adalah constraint bernama: int, float64, dan tipe turunannya.
type Number interface {
	~int | ~float64
}

// Milimeter punya underlying type int sehingga masuk himpunan ~int.
type Milimeter int

// RataRata memakai constraint bernama Number.
func RataRata[T Number](data []T) T {
	var total T
	for _, v := range data {
		total += v
	}
	return total / T(len(data))
}

// KaliDua memakai constraint literal yang ditulis langsung.
func KaliDua[T ~int | ~float64](nilai T) T {
	return nilai * 2
}

func main() {
	tinggi := []float64{1.62, 1.75, 1.8}
	fmt.Println("Rata-rata tinggi :", RataRata(tinggi))

	suhu := []int{31, 33, 34}
	fmt.Println("Rata-rata suhu   :", RataRata(suhu))

	// Milimeter lolos karena ~int menerima tipe dengan underlying int.
	tebal := []Milimeter{2, 5, 8}
	fmt.Println("Rata-rata tebal  :", RataRata(tebal))

	fmt.Println("KaliDua(21)      :", KaliDua(21))
	fmt.Println("KaliDua(2.5)     :", KaliDua(2.5))
	fmt.Println("KaliDua tebal    :", KaliDua(Milimeter(10)))
}

Output yang Dihasilkan:

text
Rata-rata tinggi : 1.7233333333333334
Rata-rata suhu   : 32
Rata-rata tebal  : 5
KaliDua(21)      : 42
KaliDua(2.5)     : 5
KaliDua tebal    : 20

Mari kita telaah baris demi baris program di atas:

  1. Baris 6-8 (type Number): constraint bernama dengan union dua elemen tilde; himpunan tipe sahnya mencakup int, float64, dan seluruh tipe turunan keduanya.
  2. Baris 11 (type Milimeter int): deklarasi tipe baru dengan underlying type int; tipe ini bukan int secara identitas, tetapi memenuhi elemen ~int.
  3. Baris 14-20 (RataRata): fungsi generik dengan constraint bernama; baris 19 mengonversi len(data) yang bertipe int menjadi T lewat T(len(data)) agar pembagian se-tipe dengan total.
  4. Baris 23 (KaliDua): constraint literal ~int | ~float64 ditulis langsung pada deklarasi fungsi tanpa interface bernama.
  5. Baris 29 dan 32: RataRata menerima []float64 dan []int — keduanya anggota langsung himpunan Number.
  6. Baris 35-36 (tebal): []Milimeter tetap diterima meskipun bukan int persis — bukti kerja tanda tilde.
  7. Baris 40 (KaliDua(Milimeter(10))): konversi Milimeter(10) membuat argumen bertipe Milimeter; hasil nilai * 2 tetap bertipe Milimeter dan tercetak 20.

Referensi #

Union dan tilde yang kita pakai di sini sebenarnya bagian dari sistem yang lebih besar bernama type sets — cara Go 1.18 mendefinisikan ulang interface menjadi method set plus himpunan tipe. Chapter Type Sets membedah sistem tersebut sampai ke irisan antar-interface.

Berkas Sumber

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

constraint-demo.go

Unduh berkas
go
package main

import "fmt"

// Number adalah constraint bernama: int, float64, dan tipe turunannya.
type Number interface {
	~int | ~float64
}

// Milimeter punya underlying type int sehingga masuk himpunan ~int.
type Milimeter int

// RataRata memakai constraint bernama Number.
func RataRata[T Number](data []T) T {
	var total T
	for _, v := range data {
		total += v
	}
	return total / T(len(data))
}

// KaliDua memakai constraint literal yang ditulis langsung.
func KaliDua[T ~int | ~float64](nilai T) T {
	return nilai * 2
}

func main() {
	tinggi := []float64{1.62, 1.75, 1.8}
	fmt.Println("Rata-rata tinggi :", RataRata(tinggi))

	suhu := []int{31, 33, 34}
	fmt.Println("Rata-rata suhu   :", RataRata(suhu))

	// Milimeter lolos karena ~int menerima tipe dengan underlying int.
	tebal := []Milimeter{2, 5, 8}
	fmt.Println("Rata-rata tebal  :", RataRata(tebal))

	fmt.Println("KaliDua(21)      :", KaliDua(21))
	fmt.Println("KaliDua(2.5)     :", KaliDua(2.5))
	fmt.Println("KaliDua tebal    :", KaliDua(Milimeter(10)))
}