Panduan Lengkap Generics di Golang: Sejarah, Konsep, dan Contoh Implementasi
Panduan mendalam implementasi Generics di Golang. Pahami cara menulis kode reusable dan aman (type-safe), pola constructor, serta update Go 1.27
Golang sejak awal dikenal karena kesederhanaannya. Sintaks yang bersih, kompilasi yang cepat, dan filosofi desain yang minimalis menjadi daya tarik utama bahasa ini. Namun, ada satu masalah fundamental yang dihadapi developer Go selama bertahun-tahun: repetisi kode untuk tipe data yang berbeda.
Ketika Anda memerlukan fungsi untuk menjumlahkan slice integer, implementasinya cukup straightforward. Tetapi ketika Anda memerlukan fungsi yang sama untuk float64, Anda harus menulis ulang fungsi tersebut dengan tipe data berbeda. Jika kemudian memerlukan varian untuk float32, proses yang sama harus diulang. Logika bisnis identik, tetapi kode harus diduplikasi berkali-kali.
Inilah yang mendorong Go team untuk akhirnya mengimplementasikan Generics, sebuah keputusan yang tidak diambil dengan mudah.
Daftar Isi
- Sejarah Singkat Generics di Go
- Konsep dan Poin-Poin Penting
- Perbandingan Implementasi: Tanpa Generics vs Dengan Generics
- Sintak Dasar Generics
- Generic Struct
- Mengapa Penulisan Tipe pada Generic Struct Wajib Eksplisit?
- Trik Menyiasati: Pola Constructor Function ( New... )
- Generic Methods (Pembaruan Besar di Go 1.27)
- Catatan Batasan Penting
- Penutup
Sejarah Singkat Generics di Go
Generics memiliki sejarah panjang dalam ekosistem Go. Rob Pike, Robert Griesemer, dan Ken Thompson sebagai perancang awal bahasa ini dengan sengaja menunda implementasi fitur ini selama lebih dari satu dekade. Pertimbangan utama mereka adalah potensi penurunan performa kompilasi dan penambahan kompleksitas pada bahasa yang dirancang untuk tetap sederhana.
Konsekuensi dari penundaan ini cukup signifikan. Developer Go harus memilih antara dua alternatif yang sama-sama memiliki kekurangan:
1. Duplikasi kode untuk setiap tipe data
Ketika Anda memerlukan fungsi untuk membalikkan urutan elemen dalam slice, Anda harus menulis implementasi terpisah untuk setiap tipe:
// Implementasi untuk integer
func ReverseInts(s []int) {
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
s[i], s[j] = s[j], s[i]
}
}
// Implementasi identik untuk string
func ReverseStrings(s []string) {
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
s[i], s[j] = s[j], s[i]
}
}Logika bisnis identik, hanya tipe data yang berbeda. Hal ini menyulitkan maintenance ketika terjadi bug atau perlu perubahan logika.
2. Penggunaan interface{} yang tidak aman
Alternatif lainnya adalah menggunakan interface{} agar dapat menerima semua tipe data. Masalahnya, pendekatan ini menghilangkan type safety:
func PrintAll(items []interface{}) {
for _, item := range items {
// Memerlukan type assertion manual
strValue := item.(string) // Berisiko: jika item bukan string, akan terjadi panic
fmt.Println(strValue)
}
}Kode di atas memang dapat dikompilasi tanpa error, namun jika ada tipe yang tidak sesuai saat runtime, aplikasi akan mengalami panic. Bug semacam ini hanya terdeteksi saat aplikasi berjalan, bukan saat kompilasi. Inilah yang disebut aplikasi menjadi tidak aman.
Konsep dan Poin-Poin Penting
Generics memungkinkan Anda menulis fungsi atau struct yang dapat bekerja dengan berbagai tipe data sambil tetap mempertahankan type safety. Berbeda dengan interface{} yang menunda validasi hingga runtime, Generics memvalidasi tipe secara penuh saat kompilasi.
Beberapa konsep penting yang perlu dipahami:
- Type Parameters - Dideklarasikan menggunakan kurung siku [] setelah nama fungsi atau tipe. Ini memberitahu compiler bahwa fungsi atau struct menerima tipe yang dinamis.
- Type Safety - Semua validasi tipe dilakukan saat kompilasi. Jika ada kesalahan tipe, compiler akan menolak kode tersebut sebelum aplikasi dijalankan.
- Keyword
any- Go 1.18 memperkenalkananysebagai alias dariinterface{}. Penggunaananymembuat kode lebih ringkas dan eksplisit. - Constraints - Anda dapat membatasi tipe apa saja yang diizinkan. Misalnya, hanya menerima tipe yang mendukung perbandingan (
comparable), atau hanya tipe numerik (int | float64).
Perbandingan Implementasi: Tanpa Generics vs Dengan Generics
Untuk memahami perbedaannya secara konkret, kita akan menggunakan contoh kasus: fungsi untuk menjumlahkan angka dalam slice.
Tanpa Generics (Pendekatan Konvensional)
Sebelum Go 1.18, jika Anda memerlukan fungsi sum untuk int dan float64, maka Anda harus menulis dua fungsi terpisah:
// Untuk integer
func SumInts(m []int) int {
var s int
for _, v := range m {
s += v
}
return s
}
// Untuk float64 - logika identik, hanya tipe berbeda
func SumFloats(m []float64) float64 {
var s float64
for _, v := range m {
s += v
}
return s
}Jika terdapat bug dalam logika penjumlahan, Anda harus memperbaikinya di dua lokasi. Jika kemudian memerlukan varian untuk float32 atau int64, fungsi baru harus ditulis lagi.

Dengan Generics
Dengan Generics, cukup satu fungsi:
func SumNumbers[T int | float64](m []T) T {
var s T
for _, v := range m {
s += v
}
return s
}Deklarasi [T int | float64] memberitahu compiler bahwa T dapat berupa int atau float64, tergantung pada argumen yang diberikan saat pemanggilan. Satu fungsi, multiple tipe. Jika ada bug, perbaikan hanya perlu dilakukan sekali saja.
Cara penggunaannya:
// Dengan slice integer
angkaInt := []int{10, 20, 30, 40}
totalInt := SumNumbers(angkaInt)
fmt.Printf("Total (Integer): %d\n", totalInt)
// Dengan slice float64
angkaFloat := []float64{10.5, 20.5, 30.5}
totalFloat := SumNumbers(angkaFloat)
fmt.Printf("Total (Float): %.2f\n", totalFloat)Compiler dapat menginferensikan tipe T berdasarkan argumen yang dikirim. Jika Anda mencoba mengirim []string, compiler akan menolaknya saat kompilasi.
Sintak Dasar Generics
Struktur dasar fungsi generic di Go:
func NamaFungsi[T BatasanTipe](parameter T) T {
// Logika fungsi di sini
}Penjelasan komponen:
[T BatasanTipe]- Deklarasi type parameter.Tadalah nama variabel tipe (konvensi menggunakan huruf kapital), dan BatasanTipe adalah constraint yang membatasi tipe yang diizinkan.(parameter T)- Parameter fungsi dengan tipe T yang dinamis.T (return type)- Nilai return menggunakan tipe T.
Contoh variasi sintak generics
1. Menerima semua tipe (any)
Digunakan ketika Anda hanya perlu memanipulasi struktur data tanpa peduli isinya:
func PrintItem[T any](item T) {
fmt.Println(item)
}2. Tipe Tertentu (Union dengan |)
Membatasi hanya untuk tipe-tipe spesifik saja:
func Concat[T string | []byte](a, b T) {
// Logika penggabungan string atau byte
}3. Custom type dengan tilde (~)
Ketika Anda membuat tipe baru berdasarkan tipe dasar, constraint biasa tidak akan mengenalinya. Tilde diperlukan:
type MyInt int
func Double[T ~int | ~float64](val T) T {
return val * 2
}Tilde (~) berarti "semua tipe yang underlying type-nya adalah int atau float64".
4. Tipe data yang dapat dibandingkan (comparable)
Keyword bawaan Go untuk tipe yang mendukung operator == dan !=:
func Contains[T comparable](slice []T, target T) bool {
for _, v := range slice {
if v == target {
return true
}
}
return false
}5. Constraint Interface
Untuk daftar tipe yang panjang, lebih baik mendefinisikan interface constraint:
type Number interface {
int | int64 | float32 | float64
}
func Add[T Number](a, b T) T {
return a + b
}Pendekatan ini lebih rapi dan dapat digunakan kembali di berbagai fungsi.
6. Multiple Type Parameters
Anda bisa menggunakan lebih dari satu type parameter dalam satu fungsi. Caranya? Pisahkan dengan koma di dalam kurung siku []. Masing-masing parameter punya "aturan main" sendiri—bisa beda-beda constraint-nya.
Kapan ini berguna?
Bayangkan Anda punya fungsi yang perlu menerima dua argumen dengan tipe berbeda. Misalnya, parameter pertama harus angka, tapi parameter kedua harus teks. Dengan multiple type parameters, Anda bisa tetap type-safe tanpa harus bikin fungsi berulang kali.
Contoh 1: Fungsi Transform (Map-like)
Ini contoh paling praktis dari multiple type parameters—fungsi yang mengubah slice dari satu tipe ke tipe lain:
// K adalah tipe input, V adalah tipe output
func Transform[K any, V any](items []K, converter func(K) V) []V {
result := make([]V, len(items))
for i, item := range items {
result[i] = converter(item)
}
return result
}
Penggunaan praktis:
angka := []int{1, 2, 3, 4, 5}
// Contoh 1: Konversi int ke string
teks := Transform(angka, func(n int) string {
return fmt.Sprintf("Angka %d", n)
})
fmt.Println(teks)
// Output: [Angka 1 Angka 2 Angka 3 Angka 4 Angka 5]
// Contoh 2: Konversi int ke float64 dengan operasi matematika
desimal := Transform(angka, func(n int) float64 {
return float64(n) * 1.5
})
fmt.Println(desimal)
// Output: [1.5 3 4.5 6 7.5]
// Contoh 3: Konversi int ke bool (ganjil/genap)
boolean := Transform(angka, func(n int) bool {
return n%2 == 0 // true jika genap
})
fmt.Println(boolean)
// Output: [false true false true false]
Perhatikan bagaimana compiler otomatis tahu tipe K dan V dari fungsi converter yang Anda berikan. Tidak perlu tulis Transform[int, string] secara eksplisit—Go cukup pintar untuk menginferensinya.
Contoh 2: Membuat Map dari Dua Slice Berbeda
Ini skenario real-world lainnya: menggabungkan dua slice menjadi sebuah map.
// K harus comparable (bisa jadi key map)
// V bisa tipe apa saja (value map)
func ZipToMap[K comparable, V any](keys []K, values []V) map[K]V {
result := make(map[K]V)
// Ambil panjang terkecil untuk menghindari index out of range
length := len(keys)
if len(values) < length {
length = len(values)
}
for i := 0; i < length; i++ {
result[keys[i]] = values[i]
}
return result
}
Penggunaan praktis:
// Contoh 1: Map nama ke umur
nama := []string{"Alice", "Bob", "Charlie"}
umur := []int{25, 30, 28}
dataUser := ZipToMap(nama, umur)
fmt.Println(dataUser)
// Output: map[Alice:25 Bob:30 Charlie:28]
// Contoh 2: Map kode negara ke populasi (dalam juta)
kodeNegara := []string{"ID", "US", "JP"}
populasi := []float64{273.8, 331.9, 125.8}
dataNegara := ZipToMap(kodeNegara, populasi)
fmt.Println(dataNegara)
// Output: map[ID:273.8 JP:125.8 US:331.9]
// Contoh 3: Map ID produk ke detail produk
idProduk := []int{101, 102, 103}
namaProduk := []string{"Laptop", "Mouse", "Keyboard"}
katalog := ZipToMap(idProduk, namaProduk)
fmt.Println(katalog)
// Output: map[101:Laptop 102:Mouse 103:Keyboard]
Contoh 3: FormatData dengan Constraint Berbeda
Sekarang mari kita lihat contoh di mana dua type parameter punya constraint yang berbeda:
// N dibatasi hanya untuk angka (int atau float64)
// S dibatasi hanya untuk teks (string atau rune)
func FormatData[N int | float64, S string | rune](angka N, teks S) string {
// %v digunakan agar fmt menyesuaikan format output dengan tipe data aslinya
return fmt.Sprintf("Keterangan: %v | Nilai: %v", teks, angka)
}
Penggunaan:
// Skenario 1: int + string
hasil1 := FormatData(100, "Kecepatan Maksimal")
fmt.Println(hasil1)
// Output: Keterangan: Kecepatan Maksimal | Nilai: 100
// Skenario 2: float64 + string
hasil2 := FormatData(36.5, "Suhu Tubuh")
fmt.Println(hasil2)
// Output: Keterangan: Suhu Tubuh | Nilai: 36.5
// Skenario 3: int + rune
hasil3 := FormatData(1, 'A')
fmt.Println(hasil3)
// Output: Keterangan: 65 | Nilai: 1
Mengapa 'A' jadi 65?
Karena rune di Go adalah alias untuk int32. Saat kita pakai %v untuk mencetak rune, Go menampilkan nilai ASCII/Unicode-nya. Huruf 'A' dalam tabel ASCII punya nilai 65.
Jika ingin tampilkan sebagai karakter, gunakan %c:
fmt.Printf("Karakter: %c | Nilai: %d\n", 'A', 1)
// Output: Karakter: A | Nilai: 1
7. Multi Parameter dan Multiple Returns
Dalam kehidupan sehari-hari sebagai developer Go, kita sering menemukan situasi di mana fungsi perlu mengembalikan lebih dari satu nilai. Contohnya? Bisa jadi kita butuh mengembalikan data beserta status validitasnya (boolean), pasangan key-value, atau hasil transformasi data yang disertai error handling.
Kabar baiknya, Generics di Go sepenuhnya mendukung konsep ini. Kita bisa mendeklarasikan beberapa nilai kembalian dengan tipe parameter yang berbeda-beda, dan semuanya akan bekerja dengan presisi.
Mari kita lihat contoh praktis. Bayangkan Anda ingin membuat fungsi yang mengambil entri pertama dari sebuah map, lalu mengembalikan key dan value-nya secara terpisah:
// K mewakili tipe key (harus comparable)
// V mewakili tipe value (bisa tipe data apa pun / any)
// Fungsi mengembalikan 3 nilai: Key (K), Value (V), dan status ketersediaan data (bool)
func GetFirstEntry[K comparable, V any](m map[K]V) (K, V, bool) {
for k, v := range m {
return k, v, true
}
// Jika map kosong, kembalikan zero-value masing-masing tipe
var zeroK K
var zeroV V
return zeroK, zeroV, false
}
// Contoh penggunaan
// Skenario 1: Map dengan Key int dan Value string
userRoles := map[int]string{
101: "Administrator",
102: "Editor",
}
id, role, ok := GetFirstEntry(userRoles)
if ok {
fmt.Printf("ID (Tipe %T): %d | Role (Tipe %T): %s\n", id, id, role, role)
// Output: ID (Tipe int): 101 | Role (Tipe string): Administrator
}
// Skenario 2: Map dengan Key string dan Value float64
stockPrices := map[string]float64{
"BBCA": 10250.50,
"TLKM": 3850.00,
}
ticker, price, ok := GetFirstEntry(stockPrices)
if ok {
fmt.Printf("Saham (Tipe %T): %s | Harga (Tipe %T): %.2f\n", ticker, ticker, price, price)
// Output: Saham (Tipe string): BBCA | Harga (Tipe float64): 10250.50
}Yang menarik di sini adalah: variabel penampung hasil fungsi (id dan role) langsung memiliki tipe konkret masing-masing (int dan string). Anda tidak perlu repot-repot melakukan konversi manual (casting) atau type assertion—semuanya sudah otomatis!
Perhatikan juga bagaimana kita menangani kasus ketika map kosong. Dengan mendeklarasikan variabel zeroK dan zeroV, kita mendapatkan zero-value dari tipe abstrak tersebut. Ini adalah cara standar di Go untuk bekerja dengan tipe generik yang membutuhkan nilai default.
8. Negative Constraints
Sekarang, ada satu hal yang perlu kita pahami: hingga artikel ini ditulis, Go belum mendukung konsep pengecualian. Maksudnya? Anda tidak bisa bilang ke compiler: "terima semua tipe data, kecuali string."
Kenapa? Karena filosofi desain Go sangat menekankan keamanan saat kompilasi dengan pendekatan allow-list (daftar yang secara eksplisit diizinkan) ketimbang block-list (daftar yang dilarang). Tim Go percaya bahwa pendekatan ini lebih aman dan lebih mudah dipahami.
Jadi, kalau Anda ingin mengecualikan satu tipe tertentu, apa solusinya? Anda harus mendaftarkan secara eksplisit semua tipe lain yang Anda izinkan. Caranya bisa dengan Union Operator (|) atau melalui Constraint Interface seperti yang sudah kita bahas di poin ke-5.
Memang terdengar sedikit merepotkan, tapi ini adalah trade-off yang disengaja demi menjaga type safety dan readability kode Anda.
Generic Struct
Selain fungsi, struct juga dapat menggunakan Generics. Ini sangat berguna untuk membuat container atau struktur data yang reusable. Contoh struct generic sederhana:
type Box[T any] struct {
Value T
}Struct Box dapat menyimpan nilai dengan tipe apapun. Cara penggunaannya:
// Box berisi integer
intBox := Box[int]{Value: 42}
fmt.Println(intBox.Value) // Output: 42
// Box berisi string
strBox := Box[string]{Value: "Hello"}
fmt.Println(strBox.Value) // Output: HelloContoh Implementasi: Stack Generic
Stack adalah struktur data LIFO (Last In First Out) yang sering kita temui. Dulu, sebelum ada Generics, kita harus membuat IntStack, StringStack, dan seterusnya untuk setiap tipe data. Sekarang? Cukup satu implementasi untuk semua:
type Stack[T any] struct {
items []T
}
func (s *Stack[T]) Push(item T) {
s.items = append(s.items, item)
}
func (s *Stack[T]) Pop() (T, bool) {
if len(s.items) == 0 {
var zero T
return zero, false
}
index := len(s.items) - 1
item := s.items[index]
s.items = s.items[:index]
return item, true
}
func (s *Stack[T]) IsEmpty() bool {
return len(s.items) == 0
}Penggunaannya:
// Stack untuk integer
intStack := Stack[int]{}
intStack.Push(10)
intStack.Push(20)
intStack.Push(30)
val, ok := intStack.Pop()
fmt.Println(val, ok) // Output: 30 true
// Stack untuk string
strStack := Stack[string]{}
strStack.Push("Go")
strStack.Push("Generics")
val2, ok2 := strStack.Pop()
fmt.Println(val2, ok2) // Output: Generics trueVariasi generic structs
1. Menerima Semua Tipe Data (any)
Cocok digunakan untuk kontainer serbaguna yang tidak memedulikan operasi internal dari data yang ditampung.
type Box[T any] struct {
Item T
}
// Penggunaan:
kotakTeks := Box[string]{Item: "Buku Panduan"}
kotakAngka := Box[int]{Item: 2026}2. Pembatasan Tipe Spesifik (Union Operator |)
Membatasi agar field dalam struct hanya bisa diisi oleh himpunan tipe data tertentu.
// ID hanya mengizinkan tipe int atau string
type Identity[T int | string] struct {
ID T
Owner string
}
// Penggunaan:
userA := Identity[int]{ID: 1001, Owner: "Budi"}
userB := Identity[string]{ID: "USR-992", Owner: "Siti"}3. Mendukung Custom Type (Simbol Tilde ~)
Jika Anda mendefinisikan custom type berbasis tipe primitif, simbol ~ memastikan tipe turunan (underlying type) tersebut tetap valid dan dapat diterima.
type Rupiah int64
type Dolar float64
// Mendukung semua custom type yang berakar dari int64 atau float64
type Wallet[T ~int64 | ~float64] struct {
Balance T
}
// Penggunaan:
dompetSaya := Wallet[Rupiah]{Balance: 500000}
dompetValas := Wallet[Dolar]{Balance: 25.50}4. Tipe yang Mendukung Komparasi (comparable)
Katakunci comparable mewajibkan tipe data mendukung operator perbandingan (== dan !=). Sangat ideal untuk struktur data berpasangan seperti key-value.
type KeyValue[K comparable, V any] struct {
Key K
Value V
}
// Method struct memanfaatkan sifat comparable milik key
func (kv KeyValue[K, V]) IsMatchingKey(targetKey K) bool {
return kv.Key == targetKey
}
// Penggunaan:
session := KeyValue[string, int]{Key: "token_abc", Value: 3600}
cocok := session.IsMatchingKey("token_abc") // true5. Constraint Interface Terpisah
Bila batasan tipe terlalu panjang, kelompokkan tipe-tipe tersebut ke dalam sebuah interface agar deklarasi struct tetap ringkas dan rapi.
type Number interface {
~int | ~int64 | ~float32 | ~float64
}
type Vector2D[T Number] struct {
X T
Y T
}
// Penggunaan:
titikPeta := Vector2D[float64]{X: -7.982, Y: 112.630}
koordinatLayar := Vector2D[int]{X: 1920, Y: 1080}6. Multiple Type Parameters
Struct dapat menerima lebih dari satu parameter tipe independen dengan memisahkannya menggunakan tanda koma.
// K dibatasi comparable untuk kategori, V berupa slice data fleksibel
type GroupedData[K comparable, V any] struct {
Category K
Items []V
}
// Penggunaan:
laporan := GroupedData[string, int]{
Category: "Penjualan Harian",
Items: []int{12, 15, 20, 18},
}Melalui kombinasi parameter tipe dan batasan (constraints) di atas, Generic Struct memberi banyak keleluasaan dalam merancang tipe data yang fleksibel. Kendati demikian, Anda mungkin memperhatikan satu perbedaan mencolok saat menginisialisasi struct generik dibanding fungsi biasa: yaitu kewajiban menuliskan parameter tipenya secara eksplisit.
Mengapa Penulisan Tipe pada Generic Struct Wajib Eksplisit?
Salah satu keterbatasan Generics di Go adalah keharusan menuliskan tipe secara eksplisit saat membuat instance dari generic struct:
// Harus eksplisit
box := Box[int]{Value: 42}
// Tidak dapat dilakukan (compiler error)
box := Box{Value: 42} // Error: cannot infer TAlasannya adalah compiler Go tidak dapat menginferensikan tipe untuk struct instantiation. Ini berbeda dengan fungsi generic yang dapat secara otomatis mendeteksi tipe dari argumen.
Banyak developer menganggap ini sebagai verbose, terutama ketika tipe datanya kompleks:
cache := Cache[map[string]interface{}]{data: make(map[string]interface{})}Trik Menyiasati: Pola Constructor Function (New...)
Solusi yang umum digunakan komunitas Go adalah membuat constructor function. Karena fungsi dapat menginferensikan tipe, kita dapat memanfaatkan mekanisme ini:
type Box[T any] struct {
Value T
}
// Constructor function
func NewBox[T any](value T) Box[T] {
return Box[T]{Value: value}
}Dengan constructor ini, Anda dapat membuat instance tanpa menuliskan tipe secara eksplisit:
// Compiler mendeteksi T = int dari argumen
box := NewBox(42)
// Compiler mendeteksi T = string dari argumen
strBox := NewBox("Hello")Pendekatan ini lebih ringkas. Pola New... telah menjadi konvensi standar di komunitas Go. Contoh untuk Stack kita sebelumnya:
func NewStack[T any]() *Stack[T] {
return &Stack[T]{
items: make([]T, 0),
}
}
// Tetap perlu eksplisit karena tidak ada argumen untuk inferensi
stack := NewStack[int]()
stack.Push(10)Untuk kasus seperti Stack yang tidak memiliki argumen initial value, penulisan tipe tetap diperlukan. Anda dapat mengatasi ini dengan menerima initial values:
func NewStackWithItems[T any](items ...T) *Stack[T] {
return &Stack[T]{
items: items,
}
}
// Inferensi tipe berhasil
stack := NewStackWithItems(1, 2, 3) // T otomatis terdeteksi sebagai intKeuntungan Pola Constructor:
- Sintaks Lebih Bersih: Mengurangi penulisan kurung siku
[...]yang berlebihan pada kode aplikasi utama. - Validasi Terpusat: Anda bisa menambahkan validasi awal (misalnya memastikan
ownertidak kosong atauidvalid) sebelum objek struct benar-benar terbentuk. - Mengembalikan Pointer dengan Rapi: Sangat mudah jika Anda ingin struct langsung dikembalikan sebagai pointer (
*Identity[T]), cukup tambahkan operator&di dalam fungsiNewIdentity.
Generic Methods (Pembaruan Besar di Go 1.27)
Go 1.27 membawa pembaruan yang signifikan. Sebelumnya, method pada struct tidak dapat memiliki type parameter tambahan selain yang sudah didefinisikan pada struct.
Sebelum Go 1.27 (Limitasi)
type Container[T any] struct {
value T
}
// Method ini valid - menggunakan T dari struct
func (c *Container[T]) Get() T {
return c.value
}
// Method dengan type parameter baru - tidak dapat dilakukan sebelum 1.27
// func (c *Container[T]) Transform[U any](fn func(T) U) U {
// return fn(c.value)
// }Jika memerlukan transformasi ke tipe lain, Anda harus membuat fungsi standalone atau menggunakan workaround.
Mulai Go 1.27 (Kemampuan Baru)
Method kini dapat memiliki type parameter sendiri:
type Container[T any] struct {
value T
}
// Method dengan type parameter baru - sekarang dimungkinkan
func (c *Container[T]) Transform[U any](fn func(T) U) Container[U] {
return Container[U]{value: fn(c.value)}
}Penggunaannya:
// Container dengan integer
intContainer := Container[int]{value: 42}
// Transform ke string
strContainer := intContainer.Transform(func(i int) string {
return fmt.Sprintf("Number: %d", i)
})
fmt.Println(strContainer.Get()) // Output: Number: 42Pembaruan ini membuka kemungkinan baru untuk functional programming patterns di Go, termasuk method chaining, map/filter/reduce operations, dan pola-pola lain yang sebelumnya sulit atau tidak mungkin diimplementasikan.
Catatan Batasan Penting
Meskipun Generics adalah fitur yang powerful, terdapat beberapa limitasi yang perlu Anda ketahui:
1. Operator Memerlukan Constraint Eksplisit.
Masalahnya: Anda tidak bisa langsung menggunakan operator seperti +, -, *, / pada type parameter any karena Go tidak tahu tipe datanya mendukung operasi tersebut atau tidak.
// Contoh salah
func Add[T any](a, b T) T {
return a + b // Error: invalid operation: operator + not defined on T
}
// Definisikan constraint yang jelas
type Numeric interface {
int | int64 | float32 | float64
}
func Add[T Numeric](a, b T) T {
return a + b // Valid!
}
// Contoh penggunaan
func main() {
fmt.Println(Add(5, 3)) // Output: 8
fmt.Println(Add(2.5, 1.7)) // Output: 4.2
}Mengapa begini? Compiler Go perlu tahu bahwa tipe T mendukung operator +. Dengan mendefinisikan constraint Numeric, kita memberitahu compiler bahwa T hanya bisa berupa tipe yang mendukung penjumlahan.
2. Interface Constraint Tidak Boleh Punya Type Parameter.
Masalahnya: Interface yang digunakan sebagai constraint tidak bisa memiliki method dengan type parameter sendiri.
Cara Yang Salah:
type Processor interface {
Process[T any](val T) T // Error: interface method cannot have type parameters
}Cara Yang Benar - Opsi 1: Type Parameter di Interface Level
// Type parameter ada di level interface, bukan di method
type Transformer[U any] interface {
Transform(val int) U
}
// Implementasi
type StringTransformer struct{}
func (s StringTransformer) Transform(val int) string {
return fmt.Sprintf("Value: %d", val)
}
// Fungsi generic yang menggunakan interface tersebut
func Apply[U any, T Transformer[U]](t T, val int) U {
return t.Transform(val)
}
// Penggunaan
func main() {
st := StringTransformer{}
result := Apply(st, 42)
fmt.Println(result) // Output: Value: 42
}Cara Yang Benar - Opsi 2: Method Non-Generic
type Processor interface {
Process(val interface{}) interface{}
}
type IntProcessor struct{}
func (p IntProcessor) Process(val interface{}) interface{} {
if v, ok := val.(int); ok {
return v * 2
}
return val
}
// Penggunaan
func main() {
proc := IntProcessor{}
result := proc.Process(21)
fmt.Println(result) // Output: 42
}3. Method Receiver Harus Memiliki Type Parameter dari Struct
Masalahnya: Anda tidak bisa menambahkan type parameter baru di method yang tidak ada di struct-nya.
type MyStruct struct {
value int
}
// Error: method receiver tidak boleh punya type parameter baru
func (m MyStruct) Convert[T any](val T) T {
return val
}Yang Benar - Opsi 1: Type Parameter di Struct
type Container[T any] struct {
value T
}
// Method bisa menggunakan T karena sudah didefinisikan di struct
func (c Container[T]) Get() T {
return c.value
}
func (c *Container[T]) Set(val T) {
c.value = val
}
// Penggunaan
func main() {
intContainer := Container[int]{value: 42}
fmt.Println(intContainer.Get()) // Output: 42
intContainer.Set(100)
fmt.Println(intContainer.Get()) // Output: 100
}Cara Yang Benar - Opsi 2: Gunakan Generic Function
type MyStruct struct {
value int
}
// Buat sebagai fungsi generic biasa, bukan method
func Convert[T any](m MyStruct, val T) T {
return val
}
// Penggunaan
func main() {
ms := MyStruct{value: 10}
result := Convert(ms, "Hello")
fmt.Println(result) // Output: Hello
}4. Tidak Ada Template Specialization
Masalahnya: Berbeda dengan C++, Go tidak mendukung specialization—Anda tidak bisa membuat versi khusus dari fungsi generic untuk tipe tertentu.
Yang Tidak Bisa Dilakukan (seperti di C++)
// Go TIDAK mendukung ini!
func Process[T any](val T) {
fmt.Println("Generic version")
}
// Tidak bisa membuat versi spesial untuk string
func Process[string](val string) { // Error: tidak valid
fmt.Println("String version:", strings.ToUpper(val))
}Cara Yang Benar - Opsi 1: Type Switch
func Process[T any](val T) {
switch v := any(val).(type) {
case string:
fmt.Println("String:", strings.ToUpper(v))
case int:
fmt.Println("Integer:", v*2)
case []int:
fmt.Println("Slice sum:", sum(v))
default:
fmt.Printf("Generic: %+v\n", val)
}
}
func sum(nums []int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
// Penggunaan
func main() {
Process("hello") // Output: String: HELLO
Process(21) // Output: Integer: 42
Process([]int{1, 2, 3}) // Output: Slice sum: 6
Process(true) // Output: Generic: true
}Cara Yang Benar - Opsi 2: Interface dengan Behavior Khusus
type Processor interface {
Process() string
}
type StringData string
func (s StringData) Process() string {
return strings.ToUpper(string(s))
}
type IntData int
func (i IntData) Process() string {
return fmt.Sprintf("Double: %d", i*2)
}
func ExecuteProcess[T Processor](val T) {
fmt.Println(val.Process())
}
// Penggunaan
func main() {
ExecuteProcess(StringData("hello")) // Output: HELLO
ExecuteProcess(IntData(21)) // Output: Double: 42
}5. Generic Type Tidak Bisa Digunakan untuk Type Assertion
Masalahnya: Anda tidak bisa melakukan type assertion langsung ke type parameter karena compiler tidak tahu tipe konkritnya saat compile time.
Cara Yang Salah
func Extract[T any](val interface{}) T {
// Error: cannot use type assertion with generic type T
return val.(T)
}Cara Yang Benar - Opsi 1: Menggunakan Reflection
import "reflect"
func Extract[T any](val interface{}) (T, error) {
var zero T
// Cek apakah tipe cocok menggunakan reflection
if reflect.TypeOf(val) == reflect.TypeOf(zero) {
return val.(T), nil
}
return zero, fmt.Errorf("type mismatch: expected %T, got %T", zero, val)
}
// Penggunaan
func main() {
result, err := Extract[int](42)
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println("Success:", result) // Output: Success: 42
}
_, err = Extract[int]("string")
if err != nil {
fmt.Println(err) // Output: type mismatch: expected int, got string
}
}Cara Yang Benar - Opsi 2: Constraint dengan Type Switch
func SafeConvert[T int | string | bool](val interface{}) (T, bool) {
var zero T
switch v := val.(type) {
case int:
if _, ok := any(zero).(int); ok {
return any(v).(T), true
}
case string:
if _, ok := any(zero).(string); ok {
return any(v).(T), true
}
case bool:
if _, ok := any(zero).(bool); ok {
return any(v).(T), true
}
}
return zero, false
}
// Penggunaan
func main() {
if val, ok := SafeConvert[int](42); ok {
fmt.Println("Converted:", val) // Output: Converted: 42
}
if _, ok := SafeConvert[int]("not an int"); !ok {
fmt.Println("Conversion failed") // Output: Conversion failed
}
}Cara Yang Benar - Opsi 3: Passing Zero Value
func TryConvert[T any](val interface{}, zero T) (T, bool) {
if v, ok := val.(T); ok {
return v, true
}
return zero, false
}
// Penggunaan
func main() {
if val, ok := TryConvert(42, 0); ok {
fmt.Println("Int:", val) // Output: Int: 42
}
if val, ok := TryConvert("hello", ""); ok {
fmt.Println("String:", val) // Output: String: hello
}
if _, ok := TryConvert(42, ""); !ok {
fmt.Println("Cannot convert int to string") // Output: Cannot convert int to string
}
}Ringkasan Praktis Batasan Go Generics
| Batasan | Masalah | Solusi | Contoh Singkat |
|---|---|---|---|
Operator pada any |
Tidak bisa a + b dengan T any |
Gunakan constraint spesifik | func Add[T int | float64](a, b T) T |
| Type parameter di method interface | Interface method tidak boleh Method[T any]() |
Pindahkan ke level interface | type Transformer[U any] interface { Transform(int) U } |
| Type parameter baru di method | Method receiver tidak boleh punya type param baru | Pindahkan ke struct atau buat fungsi | type Container[T any] struct{} lalu func (c Container[T]) Get() T |
| Specialization | Tidak ada versi khusus untuk tipe tertentu | Gunakan type switch atau interface | switch v := any(val).(type) { case string: ... } |
| Type assertion ke generic | Tidak bisa val.(T) langsung |
Gunakan reflection atau zero value | reflect.TypeOf(val) == reflect.TypeOf(zero) |
Tips Praktis
- Operator Error? → Tambahkan constraint:
int | float64 | string - Method Error? → Cek apakah type parameter ada di struct-nya
- Need Special Behavior? → Pakai type switch atau interface method
- Type Assertion Error? → Gunakan reflection atau pass zero value
- Golden Rule: Jika compiler tidak tahu tipe saat compile time, berikan constraint atau info tambahan!
Tips: Ketika menemui error generic, tanyakan diri Anda: "Apakah compiler bisa tahu informasi tipe ini saat compile time?" Jika tidak, Anda perlu memberikan constraint atau informasi tambahan.
Penutup
Generics mengubah cara penulisan kode Go secara fundamental. Duplikasi kode dan penggunaan interface{} yang tidak aman kini dapat dihindari dengan solusi yang lebih baik.
Gunakan Generics ketika:
- Anda memerlukan logika yang sama untuk multiple types
- Type safety adalah prioritas
- Anda membuat library atau utility yang reusable
Hindari Generics ketika:
- Fungsi hanya digunakan untuk satu tipe (gunakan concrete type)
- Logic untuk setiap tipe berbeda (gunakan interface)
- Penggunaan Generics justru membuat kode lebih sulit dibaca
Go 1.27 dengan fitur generic methods membuka lebih banyak kemungkinan dalam desain API. Jika Anda belum melakukan upgrade, pertimbangkan untuk melakukannya mengingat kemampuan baru yang ditawarkan.
Terima kasih, selamat mencoba dan semoga yang sedikit ini bermanfaat.
