Sebelum Go 1.18, programmer Go menghadapi dilema klasik setiap kali satu logika harus bekerja untuk banyak tipe: duplikasi kode, atau membuang informasi tipe dengan any. Generics menghadirkan jalan ketiga — satu implementasi yang tetap diperiksa compiler untuk setiap tipe pemanggilnya. Chapter ini membuktikan ketiga pendekatan dalam satu program: fungsi Max versi duplikat, versi any yang rapuh, dan versi generics yang aman, agar kita tahu persis masalah apa yang diselesaikan generics.
Masalah: Kode yang Sama untuk Tipe Berbeda #
Perhatikan dua fungsi berikut yang mengembalikan nilai terbesar dari dua bilangan:
func MaxInt(a, b int) int {
if a > b {
return a
}
return b
}
func MaxFloat64(a, b float64) float64 {
if a > b {
return a
}
return b
}Isi kedua fungsi identik baris demi baris; satu-satunya perbedaan adalah tipe parameter dan nilai baliknya. Duplikasi seperti ini menimbulkan dua masalah nyata: setiap perbaikan logika — misalnya menangani nilai NaN — harus diterapkan ke semua salinan dengan risiko satu salinan terlupakan menjadi bug tersembunyi, dan jumlah fungsi tumbuh seiring jumlah tipe (MaxInt, MaxFloat64, MaxInt64, MaxUint, …) padahal logikanya cuma satu.
Solusi Lama: any Kehilangan Type Safety #
Cara lama menghapus duplikasi adalah memakai any (alias interface{} dari chapter Tipe any
). Satu fungsi MaxAny(a, b any) any menerima semua tipe, tetapi compiler tidak tahu bahwa a dan b adalah bilangan, sehingga operator > tidak bisa dipakai sebelum nilai dikembalikan ke tipe konkretnya lewat type assertion:
na, okA := a.(float64)
nb, okB := b.(float64)
if !okA || !okB {
return nil
}Type assertion yang gagal hanya ketahuan saat program berjalan: argumen int membuat fungsi di atas diam-diam mengembalikan nil, jauh dari sumber penyebabnya. Pemanggil juga harus melakukan assertion sekali lagi untuk mengubah hasil any kembali menjadi float64 sebelum hasil tersebut bisa dihitung. Informasi tipe dibuang lalu dicari kembali — inilah yang disebut kehilangan type safety.
Solusi Go 1.18: Type Parameter #
Generics menyelesaikan kedua masalah sekaligus dengan type parameter — tipe yang menjadi parameter fungsi, sama seperti nilai menjadi parameter fungsi. Fungsi Max ditulis satu kali untuk type parameter T, dan sebuah constraint bernama Number membatasi T hanya pada int, float64, dan tipe turunannya:
type Number interface {
~int | ~float64
}
func Max[T Number](a, b T) T {
if a > b {
return a
}
return b
}Compiler memeriksa setiap pemanggilan Max terhadap himpunan tipe yang diizinkan Number. Pemanggilan Max(3, 7) dan Max(2.5, 1.5) sah, sedangkan pemanggilan dengan string ditolak sebelum program sempat berjalan. Logika duplikasi hilang, type safety tetap terjaga.
Proposal type parameters karya Ian Lance Taylor dan Robert Griesemer beredar bertahun-tahun sebelum akhirnya diterima; generics resmi dirilis pada Go 1.18, Maret 2022, dan argumentasinya dirangkum dalam tulisan resmi “Why Generics?” di blog Go.
Program Demo #
Berkas motivasi-demo.go menggabungkan ketiga versi dalam satu program:
package main
import "fmt"
// Versi 1: satu fungsi per tipe, isi kode terduplikasi.
func MaxInt(a, b int) int {
if a > b {
return a
}
return b
}
func MaxFloat64(a, b float64) float64 {
if a > b {
return a
}
return b
}
// Versi 2: satu fungsi untuk semua tipe, tapi tanpa type safety.
func MaxAny(a, b any) any {
na, okA := a.(float64)
nb, okB := b.(float64)
if !okA || !okB {
return nil
}
if na > nb {
return a
}
return b
}
// Number adalah constraint: hanya int, float64, dan tipe turunannya.
type Number interface {
~int | ~float64
}
// Versi 3: satu fungsi generik, type safety tetap terjaga.
func Max[T Number](a, b T) T {
if a > b {
return a
}
return b
}
func main() {
fmt.Println("MaxInt(3, 7) :", MaxInt(3, 7))
fmt.Println("MaxFloat64(2.5, 1.5):", MaxFloat64(2.5, 1.5))
// Versi any: bekerja untuk float64...
fmt.Println("MaxAny(2.5, 1.5) :", MaxAny(2.5, 1.5))
// ...tetapi gagal diam-diam untuk int, baru terlihat saat runtime.
fmt.Println("MaxAny(3, 7) :", MaxAny(3, 7))
// Versi generik: int dan float64 ditangani satu fungsi.
fmt.Println("Max(3, 7) :", Max(3, 7))
fmt.Println("Max(2.5, 1.5) :", Max(2.5, 1.5))
}Output yang Dihasilkan:
MaxInt(3, 7) : 7
MaxFloat64(2.5, 1.5): 2.5
MaxAny(2.5, 1.5) : 2.5
MaxAny(3, 7) : <nil>
Max(3, 7) : 7
Max(2.5, 1.5) : 2.5Mari kita telaah baris demi baris program di atas:
- Baris 6-18 (
MaxIntdanMaxFloat64): kedua fungsi berisi logika yang identik dan hanya dibedakan oleh tipe; inilah bentuk duplikasi yang ingin kita hapus. - Baris 21-31 (
MaxAny): fungsi menerimaany; baris 22-23 melakukan type assertion kefloat64dengan bentuk dua-nilai balik agar kegagalan tidak panic, dan baris 24-26 mengembalikannilketika tipe ternyata bukanfloat64. - Baris 34-36 (
type Number): constraint berbentuk interface dengan union~int | ~float64; tanda~akan kita bedah tuntas pada chapter Type Constraint . - Baris 39-44 (
Maxversi generik): kurung siku[T Number]mendeklarasikan type parameter;adanbbertipeTdan nilai balik jugaT— tipe hasil selalu sama dengan tipe masukan. - Baris 51 dan 53:
MaxAny(2.5, 1.5)berhasil karena kedua argumenfloat64;MaxAny(3, 7)mengembalikan<nil>— kegagalan baru terlihat saat runtime. - Baris 56-57 (
Max): satu fungsi yang sama menanganiintdanfloat64tanpa assertion dan tanpa duplikasi.
Generics Menang: Kesalahan Tertangkap Saat Kompilasi #
Pembuktian terkuat keunggulan generics adalah compiler yang menolak tipe di luar constraint sebelum program berjalan. Berkas error-string.go mencoba memanggil Max dengan dua string:
package main
import "fmt"
// Number adalah constraint: hanya int, float64, dan tipe turunannya.
type Number interface {
~int | ~float64
}
// Max adalah fungsi generik dengan type parameter T.
func Max[T Number](a, b T) T {
if a > b {
return a
}
return b
}
func main() {
// String bukan anggota himpunan Number.
fmt.Println(Max("apel", "bola"))
}Output yang Dihasilkan:
# command-line-arguments
./error-string.go:20:17: string does not satisfy Number (string missing in ~int | ~float64)Program gagal dikompilasi dengan pesan yang menyebut tipe penyalah (string), constraint yang dilanggar (Number), dan himpunan tipe yang diharapkan. Bandingkan dengan versi any yang mengembalikan <nil> secara diam-diam: generics memindahkan penemuan kesalahan dari runtime ke proses kompilasi.
Dengan masalah yang kini terasa nyata, langkah berikutnya adalah alat penyelesainya: cara mendeklarasikan type parameter pada fungsi dan tipe di chapter Type Parameter .
Referensi #
- Why Generics? — tulisan resmi yang merangkum masalah yang diselesaikan generics.
- Go 1.18 Release Notes — rilis yang memperkenalkan generics pada Maret 2022.
- Tutorial: Getting started with generics — tutorial resmi dengan contoh pertama.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
error-string.go
Unduh berkaspackage main
import "fmt"
// Number adalah constraint: hanya int, float64, dan tipe turunannya.
type Number interface {
~int | ~float64
}
// Max adalah fungsi generik dengan type parameter T.
func Max[T Number](a, b T) T {
if a > b {
return a
}
return b
}
func main() {
// String bukan anggota himpunan Number.
fmt.Println(Max("apel", "bola"))
}
motivasi-demo.go
Unduh berkaspackage main
import "fmt"
// Versi 1: satu fungsi per tipe, isi kode terduplikasi.
func MaxInt(a, b int) int {
if a > b {
return a
}
return b
}
func MaxFloat64(a, b float64) float64 {
if a > b {
return a
}
return b
}
// Versi 2: satu fungsi untuk semua tipe, tapi tanpa type safety.
func MaxAny(a, b any) any {
na, okA := a.(float64)
nb, okB := b.(float64)
if !okA || !okB {
return nil
}
if na > nb {
return a
}
return b
}
// Number adalah constraint: hanya int, float64, dan tipe turunannya.
type Number interface {
~int | ~float64
}
// Versi 3: satu fungsi generik, type safety tetap terjaga.
func Max[T Number](a, b T) T {
if a > b {
return a
}
return b
}
func main() {
fmt.Println("MaxInt(3, 7) :", MaxInt(3, 7))
fmt.Println("MaxFloat64(2.5, 1.5):", MaxFloat64(2.5, 1.5))
// Versi any: bekerja untuk float64...
fmt.Println("MaxAny(2.5, 1.5) :", MaxAny(2.5, 1.5))
// ...tetapi gagal diam-diam untuk int, baru terlihat saat runtime.
fmt.Println("MaxAny(3, 7) :", MaxAny(3, 7))
// Versi generik: int dan float64 ditangani satu fungsi.
fmt.Println("Max(3, 7) :", Max(3, 7))
fmt.Println("Max(2.5, 1.5) :", Max(2.5, 1.5))
}