Kenapa Generic di Golang Adalah Fitur yang Wajib Dikuasai Setiap Engineer
17 min read

Kenapa Generic di Golang Adalah Fitur yang Wajib Dikuasai Setiap Engineer

Go dikenal sebagai bahasa yang pragmatis — ia tidak menambahkan fitur baru kecuali ada alasan kuat. Ketika generics akhirnya masuk di Go 1.18 setelah bertahun-tahun perdebatan di komunitas, itu bukan keputusan ringan. Go team membutuhkan waktu hampir satu dekade untuk menemukan desain yang tepat: cukup ekspresif untuk menyelesaikan masalah nyata, tapi tidak terlalu kompleks sehingga merusak kesederhanaan Go.

Hasilnya adalah sistem generics yang berbeda dari Java, C++, atau Rust — lebih terbatas dalam beberapa hal, tapi justru lebih mudah dipahami dan diprediksi. Memahami generics di Go bukan berarti mempelajari fitur baru yang asing; lebih tepatnya, ini adalah cara baru untuk mengekspresikan pola yang selama ini sudah kamu lakukan tapi dengan duplikasi kode atau dengan mengorbankan type safety.

Artikel ini membahas generics dari fondasi sampai pola-pola production yang nyata — termasuk keterbatasan yang sering mengejutkan developer berpengalaman dari bahasa lain.


Dunia Go Sebelum Generics: Dua Jalan yang Sama Buruknya

Untuk memahami mengapa generics ada, kita perlu melihat masalah yang harus dipecahkan developer Go sebelum Go 1.18.

Bayangkan kamu perlu menulis fungsi Contains yang memeriksa apakah sebuah nilai ada dalam slice. Sebelum generics, ada dua pilihan:

Pilihan 1: Duplikasi Kode per Tipe

// ANTI-PATTERN: duplikasi untuk setiap tipe
func ContainsInt(slice []int, val int) bool {
    for _, v := range slice {
        if v == val {
            return true
        }
    }
    return false
}

func ContainsString(slice []string, val string) bool {
    for _, v := range slice {
        if v == val {
            return true
        }
    }
    return false
}

func ContainsFloat64(slice []float64, val float64) bool {
    for _, v := range slice {
        if v == val {
            return true
        }
    }
    return false
}
// ... dan seterusnya untuk setiap tipe

Logikanya identik. Hanya tipe yang berbeda. Tapi kamu harus menulis — dan merawat — tiga fungsi terpisah. Tambah satu tipe baru, tambah satu fungsi lagi. Bug di satu fungsi, harus diperbaiki di semua.

Pilihan 2: Menggunakan interface{} atau any

// ANTI-PATTERN: kehilangan type safety
func Contains(slice []interface{}, val interface{}) bool {
    for _, v := range slice {
        if v == val {
            return true
        }
    }
    return false
}

// Penggunaan — terlihat bekerja, tapi ada biaya tersembunyi
ints := []interface{}{1, 2, 3, 4, 5}
Contains(ints, 3)  // bekerja

// Tapi ini juga valid secara syntax — dan mungkin bukan yang diinginkan
Contains(ints, "tiga")  // tidak ada error compile, tapi logika salah

Dengan interface{}, kamu kehilangan kemampuan compiler untuk menangkap kesalahan tipe pada saat compile. Runtime panic atau behavior yang tidak terduga menjadi lebih mungkin. Plus ada overhead boxing/unboxing untuk tipe primitif.

Solusi dengan Generics

// BENAR: satu implementasi, type-safe untuk semua comparable type
func Contains[T comparable](slice []T, val T) bool {
    for _, v := range slice {
        if v == val {
            return true
        }
    }
    return false
}

// Semua ini valid dan type-safe
Contains([]int{1, 2, 3}, 2)             // ✓
Contains([]string{"a", "b"}, "b")       // ✓
Contains([]float64{1.1, 2.2}, 3.3)      // ✓

// Ini akan ditolak compiler — type mismatch
Contains([]int{1, 2, 3}, "dua")         // ✗ compile error

Inilah inti dari nilai generics: satu implementasi, type safety penuh, tidak ada duplikasi.


Sintaks Dasar: Type Parameter

Type parameter adalah inti dari generics. Ia menggunakan kurung siku [T Constraint] yang ditempatkan setelah nama fungsi atau tipe.

// Anatomi fungsi generic
func NamaFungsi[TypeParam Constraint](param TypeParam) TypeParam {
    // implementasi
}

Mari pecah setiap bagiannya:

func Identity[T any](val T) T {
    return val
}
  • T — nama type parameter. Konvensi menggunakan huruf kapital tunggal (T, K, V, E) tapi bisa nama apapun yang deskriptif
  • any — constraint. any adalah alias interface{} — berarti tipe apapun diterima
  • val T — parameter fungsi bertipe T
  • T (setelah kurung tutup) — return type, juga bertipe T

Memanggil Fungsi Generic

Go mendukung type inference — compiler bisa menyimpulkan type argument dari argumen yang diberikan:

// Eksplisit — menentukan type argument secara manual
Identity[int](42)
Identity[string]("halo")

// Inferensi — compiler menyimpulkan tipe dari argumen
Identity(42)        // compiler tahu T = int
Identity("halo")    // compiler tahu T = string

Dalam praktik sehari-hari, type inference hampir selalu digunakan karena lebih ringkas. Type argument eksplisit dibutuhkan hanya ketika inferensi gagal atau untuk kejelasan.


Constraint: Mendefinisikan Batas Type Parameter

Constraint adalah kontrak yang mendefinisikan operasi apa yang bisa dilakukan terhadap type parameter. Ini yang membuat generics Go berbeda dari template C++ yang lebih “bebas” — constraint harus dideklarasikan secara eksplisit.

any — Semua Tipe

// T bisa tipe apapun
func Ptr[T any](val T) *T {
    return &val
}

Dengan any, kamu hanya bisa melakukan operasi yang berlaku untuk semua tipe: assign ke variabel, pass ke fungsi lain, atau return. Kamu tidak bisa melakukan val + val karena tidak semua tipe mendukung +.

comparable — Tipe yang Bisa Dibandingkan

// T harus bisa dibandingkan dengan == dan !=
func IndexOf[T comparable](slice []T, val T) int {
    for i, v := range slice {
        if v == val {
            return i
        }
    }
    return -1
}

comparable adalah constraint bawaan Go yang mencakup semua tipe yang bisa digunakan sebagai key map: angka, string, bool, pointer, struct dengan semua field comparable.

Interface sebagai Constraint

// Constraint dengan method requirement
type Stringer interface {
    String() string
}

func Print[T Stringer](val T) {
    fmt.Println(val.String())
}

Setiap interface Go bisa digunakan sebagai constraint. Ini berarti type parameter harus mengimplementasikan interface tersebut.

Union Type dengan |

Ini adalah fitur baru yang hanya valid sebagai constraint — tidak bisa digunakan sebagai tipe biasa:

// Constraint yang menerima salah satu dari beberapa tipe konkret
type Integer interface {
    int | int8 | int16 | int32 | int64
}

type Float interface {
    float32 | float64
}

type Number interface {
    Integer | Float
}

func Sum[T Number](nums []T) T {
    var total T
    for _, n := range nums {
        total += n
    }
    return total
}

Dengan union type, kamu bisa menggunakan operator +, -, *, / karena semua tipe dalam union mendukungnya.

Tilde ~: Constraint untuk Tipe yang Underlying-nya Cocok

Tilde adalah salah satu konsep paling penting yang sering dilewatkan:

// Tanpa tilde — HANYA menerima tipe int persis
type OnlyInt interface {
    int
}

// Dengan tilde — menerima int DAN semua tipe yang underlying type-nya int
type IntLike interface {
    ~int
}

Mengapa ini penting? Karena di Go sangat umum mendefinisikan tipe kustom berbasis tipe primitif:

type UserID int
type OrderID int
type Celsius float64
type Fahrenheit float64
// ANTI-PATTERN: tanpa tilde, tipe kustom tidak diterima
type Number interface {
    int | float64
}

func Double[T Number](v T) T { return v + v }

var temp Celsius = 36.6
Double(temp)  // ✗ compile error: Celsius tidak memenuhi constraint Number
// BENAR: dengan tilde, tipe kustom dengan underlying type yang sama diterima
type Number interface {
    ~int | ~float64
}

func Double[T Number](v T) T { return v + v }

var temp Celsius = 36.6
Double(temp)  // ✓ Celsius underlying type-nya float64

Hampir selalu gunakan ~ ketika constraint melibatkan tipe primitif, kecuali kamu benar-benar hanya ingin menerima tipe persis tersebut.


Multiple Type Parameter

Fungsi dan tipe bisa memiliki lebih dari satu type parameter:

// Dua type parameter independen
func Map[T any, R any](slice []T, fn func(T) R) []R {
    result := make([]R, len(slice))
    for i, v := range slice {
        result[i] = fn(v)
    }
    return result
}

// Tiga type parameter — untuk key-value mapping
func Associate[T any, K comparable, V any](
    slice []T,
    keyFn func(T) K,
    valFn func(T) V,
) map[K]V {
    result := make(map[K]V, len(slice))
    for _, v := range slice {
        result[keyFn(v)] = valFn(v)
    }
    return result
}

Contoh penggunaan Associate:

type User struct {
    ID   int
    Name string
    Age  int
}

users := []User{
    {1, "Alice", 30},
    {2, "Bob", 25},
    {3, "Charlie", 35},
}

// Map dari ID ke nama
nameByID := Associate(users,
    func(u User) int { return u.ID },
    func(u User) string { return u.Name },
)
// nameByID = map[int]string{1: "Alice", 2: "Bob", 3: "Charlie"}

Generic Struct

Type parameter tidak hanya untuk fungsi — struct juga bisa generic:

// Generic wrapper yang bisa menyimpan nilai tipe apapun
type Optional[T any] struct {
    value    T
    hasValue bool
}

func Some[T any](val T) Optional[T] {
    return Optional[T]{value: val, hasValue: true}
}

func None[T any]() Optional[T] {
    return Optional[T]{}
}

func (o Optional[T]) Get() (T, bool) {
    return o.value, o.hasValue
}

func (o Optional[T]) OrElse(defaultVal T) T {
    if o.hasValue {
        return o.value
    }
    return defaultVal
}

Penggunaan:

name := Some("Alice")
val, ok := name.Get()  // "Alice", true

empty := None[string]()
val2 := empty.OrElse("unknown")  // "unknown"

Generic Struct untuk API Response

Pola ini sangat umum di backend Go:

type Response[T any] struct {
    Data    T      `json:"data,omitempty"`
    Error   string `json:"error,omitempty"`
    Success bool   `json:"success"`
}

func NewSuccessResponse[T any](data T) Response[T] {
    return Response[T]{Data: data, Success: true}
}

func NewErrorResponse[T any](err string) Response[T] {
    return Response[T]{Error: err, Success: false}
}

Sekarang setiap handler bisa menggunakan wrapper yang sama:

// Handler untuk user
func GetUser(w http.ResponseWriter, r *http.Request) {
    user := User{ID: 1, Name: "Alice"}
    resp := NewSuccessResponse(user)
    json.NewEncoder(w).Encode(resp)
    // {"data":{"id":1,"name":"Alice"},"success":true}
}

// Handler untuk produk — struktur response sama, tipe data berbeda
func GetProduct(w http.ResponseWriter, r *http.Request) {
    product := Product{ID: "P001", Name: "Laptop"}
    resp := NewSuccessResponse(product)
    json.NewEncoder(w).Encode(resp)
}

Constraint Komposit: Menggabungkan Beberapa Syarat

Sebuah constraint bisa menggabungkan method requirement dan union type:

// Tipe yang bisa diurutkan DAN punya method String()
type OrderedStringer interface {
    ~int | ~float64 | ~string
    String() string
}

Tapi hati-hati — constraint seperti ini sangat ketat dan jarang ada tipe yang memenuhinya. Lebih umum untuk memisahkan constraint:

// Lebih fleksibel: constraint terpisah untuk operasi yang berbeda
type Ordered interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64 |
    ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 |
    ~float32 | ~float64 | ~string
}

func Min[T Ordered](a, b T) T {
    if a < b {
        return a
    }
    return b
}

func Max[T Ordered](a, b T) T {
    if a > b {
        return a
    }
    return b
}

func Clamp[T Ordered](val, lo, hi T) T {
    return Max(lo, Min(hi, val))
}

Package golang.org/x/exp/constraints menyediakan constraint-constraint standar ini (termasuk Ordered) sehingga kamu tidak perlu mendefinisikannya sendiri.


Type Inference: Kapan Bekerja dan Kapan Tidak

Type inference di Go bekerja dengan menganalisis tipe argumen yang diberikan:

// Semua ini menggunakan type inference
nums := []int{3, 1, 4, 1, 5}
Min(3, 7)                          // T = int
Map(nums, strconv.Itoa)            // T = int, R = string (dari signature Itoa)
Contains([]string{"a", "b"}, "c") // T = string

Type inference gagal dalam beberapa situasi:

// Gagal: return type tidak bisa disimpulkan dari argumen
func Zero[T any]() T {
    var zero T
    return zero
}

Zero()        // ✗ compile error: tidak bisa menyimpulkan T
Zero[int]()   // ✓ harus eksplisit
// Gagal: tipe dalam struct literal
type Pair[A, B any] struct{ First A; Second B }

Pair{1, "hello"}           // ✗ compile error
Pair[int, string]{1, "hello"}  // ✓

Pola-Pola Production yang Berguna

1. Map, Filter, Reduce

Trio fungsi functional programming ini adalah use case klasik generics:

// Map: transformasi setiap elemen
func Map[T, R any](slice []T, fn func(T) R) []R {
    result := make([]R, len(slice))
    for i, v := range slice {
        result[i] = fn(v)
    }
    return result
}

// Filter: pilih elemen yang memenuhi predikat
func Filter[T any](slice []T, pred func(T) bool) []T {
    var result []T
    for _, v := range slice {
        if pred(v) {
            result = append(result, v)
        }
    }
    return result
}

// Reduce: akumulasi nilai
func Reduce[T, R any](slice []T, initial R, fn func(R, T) R) R {
    acc := initial
    for _, v := range slice {
        acc = fn(acc, v)
    }
    return acc
}

Contoh penggunaan nyata di service layer:

orders := []Order{
    {ID: 1, Amount: 150_000, Status: "paid"},
    {ID: 2, Amount: 200_000, Status: "pending"},
    {ID: 3, Amount: 75_000, Status: "paid"},
    {ID: 4, Amount: 300_000, Status: "paid"},
}

// Hanya order yang sudah dibayar
paidOrders := Filter(orders, func(o Order) bool {
    return o.Status == "paid"
})

// Ambil amount saja
amounts := Map(paidOrders, func(o Order) int64 {
    return o.Amount
})

// Total revenue
totalRevenue := Reduce(amounts, int64(0), func(acc, amount int64) int64 {
    return acc + amount
})
// totalRevenue = 525_000

2. Result Type untuk Error Handling yang Ekspresif

type Result[T any] struct {
    value T
    err   error
}

func Ok[T any](val T) Result[T] {
    return Result[T]{value: val}
}

func Err[T any](err error) Result[T] {
    return Result[T]{err: err}
}

func (r Result[T]) IsOk() bool { return r.err == nil }

func (r Result[T]) Unwrap() T {
    if r.err != nil {
        panic(fmt.Sprintf("called Unwrap on error Result: %v", r.err))
    }
    return r.value
}

func (r Result[T]) UnwrapOr(defaultVal T) T {
    if r.err != nil {
        return defaultVal
    }
    return r.value
}

func (r Result[T]) Error() error { return r.err }

Penggunaan:

func FetchUser(id int) Result[User] {
    user, err := db.FindUser(id)
    if err != nil {
        return Err[User](fmt.Errorf("user %d not found: %w", id, err))
    }
    return Ok(user)
}

result := FetchUser(42)
if result.IsOk() {
    user := result.Unwrap()
    fmt.Println(user.Name)
} else {
    log.Error(result.Error())
}

3. Generic Cache

type Cache[K comparable, V any] struct {
    mu    sync.RWMutex
    store map[K]V
    ttl   map[K]time.Time
    dur   time.Duration
}

func NewCache[K comparable, V any](dur time.Duration) *Cache[K, V] {
    return &Cache[K, V]{
        store: make(map[K]V),
        ttl:   make(map[K]time.Time),
        dur:   dur,
    }
}

func (c *Cache[K, V]) Set(key K, val V) {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.store[key] = val
    c.ttl[key] = time.Now().Add(c.dur)
}

func (c *Cache[K, V]) Get(key K) (V, bool) {
    c.mu.RLock()
    defer c.mu.RUnlock()
    
    exp, exists := c.ttl[key]
    if !exists || time.Now().After(exp) {
        var zero V
        return zero, false
    }
    return c.store[key], true
}

Penggunaan — satu implementasi cache untuk berbagai tipe:

// Cache untuk user dengan key int
userCache := NewCache[int, User](5 * time.Minute)
userCache.Set(42, User{ID: 42, Name: "Alice"})
user, ok := userCache.Get(42)

// Cache untuk session dengan key string
sessionCache := NewCache[string, Session](30 * time.Minute)
sessionCache.Set("token-abc", Session{UserID: 1})

4. Generic Repository Interface

type Repository[T any, ID comparable] interface {
    FindByID(ctx context.Context, id ID) (T, error)
    FindAll(ctx context.Context) ([]T, error)
    Save(ctx context.Context, entity T) error
    Delete(ctx context.Context, id ID) error
}

// Base implementation dengan operasi CRUD umum
type BaseRepository[T any, ID comparable] struct {
    db        *sql.DB
    tableName string
    idField   string
}

Setiap model bisa membuat implementasi spesifik tanpa mendefinisikan ulang kontrak dasar:

type UserRepository struct {
    BaseRepository[User, int]
}

type ProductRepository struct {
    BaseRepository[Product, string]
}

5. Pipeline dengan Generics

type Pipeline[T any] struct {
    steps []func(T) T
}

func NewPipeline[T any]() *Pipeline[T] {
    return &Pipeline[T]{}
}

func (p *Pipeline[T]) Pipe(fn func(T) T) *Pipeline[T] {
    p.steps = append(p.steps, fn)
    return p
}

func (p *Pipeline[T]) Execute(input T) T {
    result := input
    for _, step := range p.steps {
        result = step(result)
    }
    return result
}

Penggunaan untuk text processing:

pipeline := NewPipeline[string]().
    Pipe(strings.TrimSpace).
    Pipe(strings.ToLower).
    Pipe(func(s string) string { return strings.ReplaceAll(s, " ", "-") })

slug := pipeline.Execute("  Hello World  ")
// slug = "hello-world"

Generic vs Interface: Kapan Menggunakan Masing-masing

Ini adalah pertanyaan paling sering muncul. Jawabannya tidak selalu hitam-putih, tapi ada panduan yang cukup jelas:

SituasiGunakanAlasan
Mendefinisikan perilaku yang harus diimplementasikanInterfacePolimorfisme runtime — tipe berbeda bisa diperlakukan sama
Bekerja dengan data dari berbagai tipeGenericType safety compile-time, tidak ada overhead boxing
Koleksi heterogen (berbeda tipe dalam satu slice)InterfaceGeneric slice []T hanya bisa menyimpan satu tipe
Algoritma independen dari tipeGenericSatu implementasi untuk semua tipe yang memenuhi constraint
Dependency injectionInterfaceMemungkinkan swapping implementasi saat runtime atau test
Utility functions (Map, Filter, Sort)GenericLogika sama, tipe data berbeda
// Interface — tepat untuk mendefinisikan behavior
type PaymentGateway interface {
    Charge(amount int64, currency string) (TransactionID, error)
    Refund(txID TransactionID) error
}

// Berbagai implementasi bisa digunakan secara interchanageable
var gateway PaymentGateway
gateway = &StripeGateway{}
gateway = &MidtransGateway{}
gateway = &MockGateway{} // untuk testing

// Generic — tepat untuk algoritma yang bekerja pada data
func SortBy[T any](slice []T, less func(a, b T) bool) []T {
    result := make([]T, len(slice))
    copy(result, slice)
    sort.Slice(result, func(i, j int) bool {
        return less(result[i], result[j])
    })
    return result
}

// Satu fungsi untuk sort berbagai tipe
sortedUsers := SortBy(users, func(a, b User) bool { return a.Name < b.Name })
sortedOrders := SortBy(orders, func(a, b Order) bool { return a.Amount > b.Amount })

Keterbatasan Generics di Go

Go membuat pilihan desain yang sadar untuk membatasi beberapa hal. Memahami keterbatasan ini mencegah frustrasi:

1. Method Tidak Bisa Punya Type Parameter Sendiri

type MySlice[T any] struct {
    data []T
}

// ✗ TIDAK VALID di Go — method tidak bisa punya type parameter baru
func (s MySlice[T]) Map[R any](fn func(T) R) []R {
    // compile error
}

// ✓ Solusi: gunakan fungsi top-level, bukan method
func MapSlice[T, R any](s MySlice[T], fn func(T) R) []R {
    result := make([]R, len(s.data))
    for i, v := range s.data {
        result[i] = fn(v)
    }
    return result
}

Ini adalah keterbatasan yang disengaja untuk menjaga kesederhanaan compiler Go. Banyak developer dari Java atau Kotlin merasa ini membatasi, tapi dalam praktiknya solusi fungsi top-level sering lebih jelas.

2. Tidak Ada Specialization

Di C++, kamu bisa memberikan implementasi khusus untuk tipe tertentu. Go tidak mendukung ini:

// ✗ Tidak bisa membuat versi khusus untuk tipe tertentu
// Tidak ada template specialization seperti C++

3. Type Switch Tidak Bekerja pada Type Parameter

func Process[T any](val T) string {
    // ✗ Ini tidak bekerja seperti yang diharapkan
    switch v := any(val).(type) {  // harus cast ke any dulu
    case int:
        return fmt.Sprintf("int: %d", v)
    case string:
        return fmt.Sprintf("string: %s", v)
    }
    return "unknown"
}

Jika kamu perlu type switch, itu sinyal bahwa generic mungkin bukan solusi yang tepat untuk kasus ini.

4. Tidak Bisa Instantiate Type Parameter

func NewSlice[T any](size int) []T {
    return make([]T, size)  // ✓ make bekerja
}

func NewStruct[T any]() T {
    return T{}  // ✓ zero value bekerja
}

// ✗ Tidak bisa memanggil constructor yang mungkin tidak ada
func New[T any]() T {
    return T.New()  // compile error
}

Untuk membuat instance dengan inisialisasi kustom, gunakan fungsi factory sebagai parameter:

func NewSliceOf[T any](size int, factory func() T) []T {
    result := make([]T, size)
    for i := range result {
        result[i] = factory()
    }
    return result
}

Anti-Pattern Generics yang Harus Dihindari

1. Over-Generalization

// ANTI-PATTERN: generic untuk fungsi yang hanya digunakan sekali
func AddInts[T ~int](a, b T) T {
    return a + b
}

// Kamu tidak pernah memanggil ini dengan tipe lain selain int
// BENAR: cukup gunakan fungsi biasa
func AddInts(a, b int) int {
    return a + b
}

Generic menambahkan kompleksitas kognitif. Jika tidak ada manfaat nyata dari type flexibility, jangan gunakan generic.

2. Constraint Terlalu Longgar

// ANTI-PATTERN: menggunakan any tapi melakukan operasi yang tidak didukung semua tipe
func Double[T any](val T) T {
    return val + val  // ✗ compile error: operator + tidak didefinisikan untuk any
}

// BENAR: constraint yang tepat
type Addable interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64 |
    ~float32 | ~float64 | ~string
}

func Double[T Addable](val T) T {
    return val + val  // ✓
}

3. Menggantikan Interface dengan Generic untuk Dependency Injection

// ANTI-PATTERN: generic untuk sesuatu yang seharusnya interface
type Service[DB any] struct {
    db DB
}

func (s Service[DB]) GetUser(id int) User {
    // bagaimana cara memanggil method DB? T = any, tidak ada kontrak
}

// BENAR: gunakan interface untuk mendefinisikan kontrak behavior
type Database interface {
    QueryRow(query string, args ...any) *sql.Row
    Exec(query string, args ...any) (sql.Result, error)
}

type Service struct {
    db Database
}

4. Generics untuk Menghindari Menulis Tipe Konkret

// ANTI-PATTERN: generic hanya untuk menghindari menuliskan tipe
func GetConfig[T any](key string) T {
    // implementasi yang melakukan type assertion di dalamnya
    val := config[key]
    return val.(T)  // type assertion saat runtime — ini bukan type safety!
}

// Ini tidak memberikan manfaat compile-time safety yang dijanjikan generics
// BENAR: definisikan accessor yang spesifik atau gunakan pola yang jelas
func GetStringConfig(key string) (string, error) { ... }
func GetIntConfig(key string) (int, error) { ... }

Diagram: Kapan Memilih Generic, Interface, atau Tipe Konkret

flowchart TD
    A{Logika sama\nuntuk tipe berbeda?} -- Tidak --> B[Gunakan tipe konkret]
    A -- Ya --> C{Butuh koleksi\nheterogen?}
    C -- Ya --> D[Gunakan Interface]
    C -- Tidak --> E{Mendefinisikan\nperilaku/behavior?}
    E -- Ya --> D
    E -- Tidak --> F{Butuh type safety\ncompile-time?}
    F -- Ya --> G[Gunakan Generic]
    F -- Tidak --> H{Performa\nkritical?}
    H -- Ya --> G
    H -- Tidak --> D

Ringkasan

  • Dua masalah yang dipecahkan generics: duplikasi kode per tipe dan kehilangan type safety akibat interface{}/any — generics memberikan solusi yang menggabungkan keunggulan keduanya.
  • Sintaks dasar: func Nama[T Constraint](param T) T — kurung siku setelah nama fungsi untuk type parameter, diikuti constraint.
  • Tilde ~ adalah kunci: ~int menerima int dan semua tipe kustom yang underlying type-nya int — hampir selalu gunakan ~ untuk constraint tipe primitif.
  • comparable adalah constraint bawaan untuk tipe yang bisa digunakan sebagai map key (== dan != — berguna untuk fungsi seperti Contains, IndexOf, dan semua struktur map-based.
  • Type inference bekerja dari argumen fungsi — kamu hampir tidak perlu menuliskan type argument secara eksplisit kecuali return-only type parameter.
  • Method tidak bisa punya type parameter baru — ini keterbatasan yang disengaja; gunakan fungsi top-level sebagai gantinya.
  • Generic vs Interface: gunakan interface untuk mendefinisikan behavior (polimorfisme runtime, dependency injection), gunakan generic untuk algoritma dan data (Map, Filter, cache, collection utils).
  • Pola production paling berguna: Map/Filter/Reduce, Optional[T], Result[T], generic cache dengan TTL, dan Response[T] untuk API response wrapper.
  • Anti-pattern utama: over-generalize fungsi yang hanya dipakai sekali, constraint terlalu longgar, menggunakan generic sebagai pengganti interface untuk dependency injection, dan melakukan type assertion di dalam fungsi generic.

Portofolio