Constraint seperti ~int | ~float64 terlihat seperti daftar belanja tipe, tetapi di baliknya ada model konseptual yang lebih dalam: sejak Go 1.18, setiap interface mendefinisikan type set — himpunan tipe konkret yang memenuhi interface tersebut. Model ini menjelaskan mengapa operator tertentu sah pada type parameter, mengapa irisan interface bisa mempersempit himpunan, dan mengapa union int | string hanya mengizinkan operasi yang berlaku untuk keduanya. Chapter ini membedah type sets lapis demi lapis dan membuktikan setiap aturan lewat compiler yang asli.

Interface Mendefinisikan Dua Hal #

Sebelum generics, interface didefinisikan sebagai method set: kumpulan method yang harus dimiliki suatu tipe. Go 1.18 memperluas definisi tersebut menjadi dua bagian:

go
type Berlabel interface {
	String() string
}

Interface Berlabel tetap punya method set String() string, dan type set-nya mencakup semua tipe yang mengimplementasikan method tersebut. Interface constraint menambahkan jenis elemen baru — union tipe — yang membangun type set secara langsung tanpa method:

go
type Bilangan interface {
	~int | ~float64
}

Type set Bilangan adalah himpunan semua tipe dengan underlying type int atau float64. Sebuah tipe memenuhi constraint jika tipe tersebut termasuk anggota type set constraint, dan sebuah operasi pada type parameter sah jika operasi itu valid untuk setiap anggota himpunan.

Elemen Union dan Elemen Tilde #

Union menyusun himpunan dengan operator “atau”: ~int | ~float64 berarti anggota himpunan adalah gabungan semua tipe ber-underlying int dan semua tipe ber-underlying float64. Elemen tilde ~T menyeleksi berdasarkan underlying type, sehingga tipe buatan seperti type Skor int masuk himpunan ~int. Elemen tanpa tilde seperti int pada int | string menyeleksi tipe persis — hanya int identitas yang lolos, bukan turunannya.

Irisan: Menyempitkan Himpunan dengan Embedding #

Interface bisa meng-embed interface lain, dan ketika constraint meng-embed lebih dari satu interface, type set yang terbentuk adalah irisan himpunan-himpunan tersebut:

go
type BulatBerlabel interface {
	Berlabel
	~int
}

Constraint BulatBerlabel hanya menerima tipe yang sekaligus punya method String dan ber-underlying int — kombinasi yang sulit dicapai satu union, karena union hanya menambah anggota sedangkan irisan memangkas. Tipe Skor pada program demo adalah anggota tepat irisan ini.

Operasi Sah Ditentukan oleh Seluruh Himpunan #

Aturan inti type sets: operator pada type parameter sah hanya jika valid untuk semua anggota himpunan. Union int | string adalah contoh pembelajaran yang menarik: operator + berlaku untuk int dan string sehingga boleh dipakai, sedangkan * hanya berlaku untuk int — satu anggota yang tidak mendukung sudah cukup menolak seluruh operasi, seperti dibuktikan berkas error-operator.go:

go
package main

import "fmt"

// Tempel memakai union int | string tanpa tilde.
func Tempel[T int | string](a, b T) T {
	// Operator * tidak sah untuk string sehingga ditolak compiler,
	// walaupun T bisa berupa int.
	return a * b
}

func main() {
	fmt.Println(Tempel(10, 32))
}

Output yang Dihasilkan:

text
# command-line-arguments
./error-operator.go:9:9: invalid operation: operator * not defined on a (variable of type T constrained by int | string)

Pesan tersebut menyebut constraint secara eksplisit: T constrained by int | string. Union semacam ini jarang bernilai — constraint berguna ketika himpunannya mendukung operasi yang dibutuhkan isi fungsi.

Interface Biasa vs Interface Constraint #

Type set tidak membuat semua interface menjadi constraint. Interface yang elemennya hanya method — seperti Berlabel — tetap berperilaku sebagai interface biasa: boleh menjadi tipe variabel, parameter, dan nilai balik. Sebaliknya, interface yang memuat union atau tilde hanya boleh dipakai sebagai constraint: deklarasi var x interface{ ~int } ditolak compiler dengan pesan cannot use type interface{~int} outside a type constraint: interface contains type constraints.

Program Demo #

Berkas typesets-demo.go menggabungkan union, tilde, dan irisan dalam satu program:

go
package main

import "fmt"

// Berlabel: interface biasa yang hanya berisi method set.
type Berlabel interface {
	String() string
}

// Bilangan: constraint dengan union dua elemen tilde.
type Bilangan interface {
	~int | ~float64
}

// Skor: tipe dengan underlying int sekaligus method String.
type Skor int

func (s Skor) String() string {
	return fmt.Sprintf("skor %d", int(s))
}

// BulatBerlabel adalah irisan: underlying int DAN memiliki method String.
type BulatBerlabel interface {
	Berlabel
	~int
}

// Operator aritmetika sah karena seluruh anggota himpunan ber-underlying int.
func Naikkan[L BulatBerlabel](nilai L) L {
	return nilai + 1
}

// Operator tambah sah untuk seluruh anggota himpunan Bilangan.
func Gabung[B Bilangan](a, b B) B {
	return a + b
}

// Tempel memakai union int | string tanpa tilde.
// Operator + sah karena berlaku untuk int maupun string.
func Tempel[T int | string](a, b T) T {
	return a + b
}

func main() {
	var ujian Skor = 87
	naik := Naikkan(ujian)
	fmt.Println(naik.String())

	fmt.Println(Gabung(10, 32))
	fmt.Println(Gabung(1.5, 2.25))

	fmt.Println(Tempel(10, 32))
	fmt.Println(Tempel("go", "lang"))
}

Output yang Dihasilkan:

text
skor 88
42
3.75
42
golang

Mari kita telaah baris demi baris program di atas:

  1. Baris 6-8 (type Berlabel): interface method-only; type set-nya mencakup semua tipe punya String() string, termasuk Skor.
  2. Baris 11-13 (type Bilangan): constraint union dengan dua elemen tilde; type set-nya gabungan tipe ber-underlying int dan float64.
  3. Baris 16-20 (type Skor dan method): tipe bernama ber-underlying int yang juga mengimplementasikan String — anggota dari Bilangan sekaligus BulatBerlabel.
  4. Baris 23-26 (type BulatBerlabel): irisan dua interface; baris 24 meng-embed Berlabel dan baris 25 menambah elemen ~int, sehingga hanya tipe ber-underlying int punya String() yang lolos.
  5. Baris 29-31 (Naikkan): operator + pada baris 30 sah karena semua anggota BulatBerlabel ber-underlying int; nilai balik tetap bertipe L sehingga Skor masuk keluar tanpa konversi.
  6. Baris 46-47 (Naikkan(ujian)): T tersimpulkan sebagai Skor; hasil 87 + 1 bertipe Skor dan method String mencetak skor 88.
  7. Baris 52-53 (Tempel): satu fungsi yang sama menjumlahkan int dan menggabungkan string karena + valid untuk semua anggota himpunan int | string.

Referensi #

Model type sets kini menjadi fondasi untuk praktik: menyusun fungsi generik produksi seperti Map, Filter, dan Reduce. Chapter Generic Function menyusun ketiganya dengan type parameter ganda dan membuktikan kualitasnya lewat inferensi tipe.

Berkas Sumber

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

error-operator.go

Unduh berkas
go
package main

import "fmt"

// Tempel memakai union int | string tanpa tilde.
func Tempel[T int | string](a, b T) T {
	// Operator * tidak sah untuk string sehingga ditolak compiler,
	// walaupun T bisa berupa int.
	return a * b
}

func main() {
	fmt.Println(Tempel(10, 32))
}

typesets-demo.go

Unduh berkas
go
package main

import "fmt"

// Berlabel: interface biasa yang hanya berisi method set.
type Berlabel interface {
	String() string
}

// Bilangan: constraint dengan union dua elemen tilde.
type Bilangan interface {
	~int | ~float64
}

// Skor: tipe dengan underlying int sekaligus method String.
type Skor int

func (s Skor) String() string {
	return fmt.Sprintf("skor %d", int(s))
}

// BulatBerlabel adalah irisan: underlying int DAN memiliki method String.
type BulatBerlabel interface {
	Berlabel
	~int
}

// Operator aritmetika sah karena seluruh anggota himpunan ber-underlying int.
func Naikkan[L BulatBerlabel](nilai L) L {
	return nilai + 1
}

// Operator tambah sah untuk seluruh anggota himpunan Bilangan.
func Gabung[B Bilangan](a, b B) B {
	return a + b
}

// Tempel memakai union int | string tanpa tilde.
// Operator + sah karena berlaku untuk int maupun string.
func Tempel[T int | string](a, b T) T {
	return a + b
}

func main() {
	var ujian Skor = 87
	naik := Naikkan(ujian)
	fmt.Println(naik.String())

	fmt.Println(Gabung(10, 32))
	fmt.Println(Gabung(1.5, 2.25))

	fmt.Println(Tempel(10, 32))
	fmt.Println(Tempel("go", "lang"))
}