Generic — dan kapan sebaiknya tidak
Generic menyelesaikan satu masalah nyata: menulis fungsi yang sama untuk banyak tipe tanpa kehilangan pemeriksaan compiler. Ia bukan pengganti interface, dan bukan tempat memindahkan kebiasaan dari bahasa lain.
Intisari
- Sintaksnya:
func Peta[T, U any](s []T, f func(T) U) []U. Parameter tipe di dalam kurung siku. any,comparable, dancmp.Orderedmenutupi hampir semua kebutuhan batasan tipe.- Pakai generic saat algoritmanya sama untuk banyak tipe. Pakai interface saat perilakunya berbeda.
- Generic tidak menggantikan
io.Reader: kalau tipe-tipe itu punya method, interface tetap jawabannya. - Go 1.26 melonggarkan aturan sehingga tipe generic boleh menyebut dirinya sendiri di daftar parameter tipe.
Bentuk dasarnya
// T adalah parameter tipe; "any" adalah batasannya (tanpa batasan).
func Pertama[T any](s []T) (T, bool) {
var nol T
if len(s) == 0 {
return nol, false
}
return s[0], true
}
n, ok := Pertama([]int{1, 2, 3}) // T disimpulkan: int
s, ok := Pertama([]string{"a", "b"}) // T disimpulkan: string
Perhatikan var nol T: dalam fungsi generic kamu tidak bisa menulis 0 atau
"", karena tipenya belum diketahui. Nilai nol dari parameter tipe selalu ditulis begitu.
Batasan tipe
| Batasan | Mengizinkan | Dari paket |
|---|---|---|
any | Tipe apa pun | bawaan |
comparable | Yang bisa dibandingkan dengan == (kunci map) | bawaan |
cmp.Ordered | Yang bisa dibandingkan dengan <: angka dan string | cmp |
| Interface method | Tipe yang punya method tersebut | milikmu |
Union: ~int | ~string | Daftar tipe tertentu; ~ berarti termasuk tipe bernama di atasnya | milikmu |
import "cmp"
func Maks[T cmp.Ordered](s []T) T {
hasil := s[0]
for _, v := range s[1:] {
if v > hasil {
hasil = v
}
}
return hasil
}
// Batasan buatan sendiri. Tilde (~) membuat "type Rupiah int64" ikut masuk.
type Angka interface {
~int | ~int64 | ~float64
}
func Jumlah[T Angka](s []T) T {
var total T
for _, v := range s {
total += v
}
return total
}
Jumlah([]Rupiah{25000, 15000}) // bekerja berkat ~int64
Yang benar-benar berguna di aplikasi web
// 1. Respons API terbungkus — hilangkan belasan struct yang isinya sama
type Halaman[T any] struct {
Data []T `json:"data"`
Berikut string `json:"berikutnya,omitempty"`
Total int `json:"total"`
}
func kirim[T any](w http.ResponseWriter, status int, v T) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
return json.NewEncoder(w).Encode(v)
}
// 2. Nilai opsional untuk PATCH — bedakan "tidak dikirim" dari "dikirim null"
type Opsional[T any] struct {
Nilai T
Ada bool
}
// 3. Cache bertipe, tanpa type assertion di setiap pemakaian
type Cache[K comparable, V any] struct {
mu sync.RWMutex
data map[K]V
}
func (c *Cache[K, V]) Ambil(k K) (V, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
v, ok := c.data[k]
return v, ok
}
Kapan generic adalah jawaban yang salah
| Kalau kamu… | Yang sebenarnya kamu butuhkan |
|---|---|
Menulis [T any] lalu langsung switch v.(type) | Interface, atau fungsi terpisah |
| Punya satu tipe yang memakainya | Tulis untuk tipe itu saja. Generalisasikan saat pemakai kedua muncul |
Membuat Repository[T] generik untuk semua entitas | Repository konkret per domain — kuerinya memang berbeda |
Membuat Map, Filter, Reduce sendiri | Perulangan for biasa; ia lebih mudah dibaca di Go |
| Ingin "menghilangkan duplikasi" dua fungsi yang mirip | Duplikasi kecil lebih murah daripada abstraksi yang salah |
Pedoman resmi tim Go: tulis kodenya dulu secara konkret. Kalau badan fungsinya ternyata identik untuk beberapa tipe — bukan cuma bentuknya — barulah angkat jadi generic. Kalau badannya berbeda per tipe, yang kamu butuhkan interface.
Generic tidak menggantikan interface
// ❌ tidak ada gunanya: tidak ada perilaku yang dibagi
func Tulis[T io.Writer](w T, b []byte) { w.Write(b) }
// ✅ interface biasa, dan lebih fleksibel
func Tulis(w io.Writer, b []byte) { w.Write(b) }
Aturan pembeda yang mudah diingat: generic untuk tipe data, interface untuk perilaku. Kalau yang berbeda antar tipe adalah apa yang dilakukannya, itu interface. Kalau yang berbeda cuma apa isinya, itu generic.
Biaya yang perlu diketahui
- Compiler menghasilkan satu versi kode per bentuk tipe (semua pointer berbagi satu versi), sehingga generic dengan banyak tipe nilai bisa menambah ukuran binari.
- Pesan error kompilasi pada generic yang bersarang jauh lebih sulit dibaca daripada kode biasa.
- Method tidak boleh punya parameter tipe sendiri — hanya fungsi dan tipe yang boleh. Ini pembatas nyata saat merancang API.
Perubahan di Go 1.26: larangan tipe generic menyebut dirinya sendiri di daftar parameter tipe
dicabut, sehingga pola seperti type Adder[A Adder[A]] interface { Add(A) A } sekarang sah.
Ini melonggarkan batasan yang selama ini menghalangi beberapa pola API bertipe kuat.
Latihan: tulis func Kelompokkan[T any, K comparable](s []T, kunci func(T) K) map[K][]T
dan pakai untuk mengelompokkan slice Produk berdasarkan kategorinya. Lalu coba tulis fungsi
yang sama sebagai method dari sebuah struct dengan parameter tipe sendiri — baca pesan error
compiler-nya, dan ingat batasan itu.
Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.