Generik Go: Kasus Penggunaan dan Pola

Kode yang aman secara tipis dan dapat digunakan kembali dengan Go generics

Konten Halaman

Generik dalam Go mewakili salah satu fitur bahasa paling signifikan yang ditambahkan sejak Go 1.0. Diperkenalkan dalam Go 1.18, generik memungkinkan Anda menulis kode yang aman secara tipe dan dapat digunakan kembali yang bekerja dengan berbagai tipe tanpa mengorbankan kinerja atau kejelasan kode.

Artikel ini menjelaskan kasus penggunaan praktis, pola umum, dan praktik terbaik dalam memanfaatkan generik dalam program Go Anda. Jika Anda baru dengan Go atau membutuhkan refresher tentang dasar-dasarnya, lihat Go Cheatsheet kami untuk konstruksi bahasa dan sintaks penting.

a-lone-programmer

Memahami Generik dalam Go

Generik dalam Go memungkinkan Anda menulis fungsi dan tipe yang diparameterisasi oleh parameter tipe. Ini menghilangkan kebutuhan untuk duplikasi kode ketika Anda membutuhkan logika yang sama untuk bekerja dengan berbagai tipe, sambil mempertahankan keamanan tipe pada saat kompilasi.

Sintaks Dasar

Sintaks untuk generik menggunakan kurung siku untuk menyatakan parameter tipe:

// Fungsi generik
func Max[T comparable](a, b T) T {
    if a > b {
        return a
    }
    return b
}

// Penggunaan
maxInt := Max(10, 20)
maxString := Max("apple", "banana")

Batasan Tipe

Batasan tipe menentukan tipe apa yang dapat digunakan dengan kode generik Anda:

  • any: Tipe apa pun (setara dengan interface{})
  • comparable: Tipe yang mendukung operator == dan !=
  • Batasan antarmuka kustom: Definisikan persyaratan Anda sendiri
// Menggunakan batasan kustom
type Numeric interface {
    int | int8 | int16 | int32 | int64 | 
    uint | uint8 | uint16 | uint32 | uint64 | 
    float32 | float64
}

func Sum[T Numeric](numbers []T) T {
    var sum T
    for _, n := range numbers {
        sum += n
    }
    return sum
}

Kasus Penggunaan Umum

1. Struktur Data Generik

Salah satu kasus penggunaan paling menarik untuk generik adalah menciptakan struktur data yang dapat digunakan kembali:

// Tumpukan generik
type Stack[T any] struct {
    items []T
}

func NewStack[T any]() *Stack[T] {
    return &Stack[T]{items: make([]T, 0)}
}

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
    }
    item := s.items[len(s.items)-1]
    s.items = s.items[:len(s.items)-1]
    return item, true
}

// Penggunaan
intStack := NewStack[int]()
intStack.Push(42)
intStack.Push(100)

stringStack := NewStack[string]()
stringStack.Push("hello")

2. Utilitas Slice

Generik memudahkan penulisan fungsi manipulasi slice yang dapat digunakan kembali:

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

// Fungsi Filter
func Filter[T any](slice []T, fn func(T) bool) []T {
    var result []T
    for _, v := range slice {
        if fn(v) {
            result = append(result, v)
        }
    }
    return result
}

// Fungsi Reduce
func Reduce[T, U any](slice []T, initial U, fn func(U, T) U) U {
    result := initial
    for _, v := range slice {
        result = fn(result, v)
    }
    return result
}

// Penggunaan
numbers := []int{1, 2, 3, 4, 5}
doubled := Map(numbers, func(n int) int { return n * 2 })
evens := Filter(numbers, func(n int) bool { return n%2 == 0 })
sum := Reduce(numbers, 0, func(acc, n int) int { return acc + n })

3. Utilitas Peta Generik

Bekerja dengan peta menjadi lebih aman secara tipe dengan generik:

// Dapatkan kunci peta sebagai slice
func Keys[K comparable, V any](m map[K]V) []K {
    keys := make([]K, 0, len(m))
    for k := range m {
        keys = append(keys, k)
    }
    return keys
}

// Dapatkan nilai peta sebagai slice
func Values[K comparable, V any](m map[K]V) []V {
    values := make([]V, 0, len(m))
    for _, v := range m {
        values = append(values, v)
    }
    return values
}

// Dapatkan nilai peta dengan nilai default
func GetOrDefault[K comparable, V any](m map[K]V, key K, defaultValue V) V {
    if v, ok := m[key]; ok {
        return v
    }
    return defaultValue
}

4. Pola Pilihan Generik

Pola Pilihan menjadi lebih elegan dengan generik:

type Option[T any] struct {
    value *T
}

func Some[T any](value T) Option[T] {
    return Option[T]{value: &value}
}

func None[T any]() Option[T] {
    return Option[T]{value: nil}
}

func (o Option[T]) IsSome() bool {
    return o.value != nil
}

func (o Option[T]) IsNone() bool {
    return o.value == nil
}

func (o Option[T]) Unwrap() T {
    if o.value == nil {
        panic("attempted to unwrap None value")
    }
    return *o.value
}

func (o Option[T]) UnwrapOr(defaultValue T) T {
    if o.value == nil {
        return defaultValue
    }
    return *o.value
}

Pola Lanjutan

Komposisi Batasan

Anda dapat menggabungkan batasan untuk menciptakan persyaratan yang lebih spesifik:

type Addable interface {
    int | int8 | int16 | int32 | int64 |
    uint | uint8 | uint16 | uint32 | uint64 |
    float32 | float64 | string
}

type Multiplicable interface {
    int | int8 | int16 | int32 | int64 |
    uint | uint8 | uint16 | uint32 | uint64 |
    float32 | float64
}

type Numeric interface {
    Addable
    Multiplicable
}

func Multiply[T Multiplicable](a, b T) T {
    return a * b
}

Antarmuka Generik

Antarmuka juga dapat generik, memungkinkan abstraksi yang kuat:

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

// Implementasi
type InMemoryRepository[T any, ID comparable] struct {
    data map[ID]T
}

func NewInMemoryRepository[T any, ID comparable]() *InMemoryRepository[T, ID] {
    return &InMemoryRepository[T, ID]{
        data: make(map[ID]T),
    }
}

func (r *InMemoryRepository[T, ID]) FindByID(id ID) (T, error) {
    if entity, ok := r.data[id]; ok {
        return entity, nil
    }
    var zero T
    return zero, fmt.Errorf("entity not found")
}

Inferensi Tipe

Inferensi tipe Go sering memungkinkan Anda menghilangkan parameter tipe eksplisit:

// Inferensi tipe dalam aksi
numbers := []int{1, 2, 3, 4, 5}

// Tidak perlu menyebutkan [int] - Go menginferensikannya
doubled := Map(numbers, func(n int) int { return n * 2 })

// Parameter tipe eksplisit ketika diperlukan
result := Map[int, string](numbers, strconv.Itoa)

Praktik Terbaik

1. Mulai Sederhana

Jangan terlalu memakai generik. Jika antarmuka sederhana atau tipe konkret akan bekerja, pilih itu untuk membaca yang lebih baik:

// Lebih disukai untuk kasus sederhana
func Process(items []Processor) {
    for _, item := range items {
        item.Process()
    }
}

// Terlalu generik - hindari
func Process[T Processor](items []T) {
    for _, item := range items {
        item.Process()
    }
}

2. Gunakan Nama Batasan yang Berarti

Berikan nama batasan secara jelas untuk menyampaikan niat:

// Baik
type Sortable interface {
    int | int8 | int16 | int32 | int64 |
    uint | uint8 | uint16 | uint32 | uint64 |
    float32 | float64 | string
}

// Kurang jelas
type T interface {
    int | string
}

3. Dokumentasikan Batasan yang Kompleks

Ketika batasan menjadi kompleks, tambahkan dokumentasi:

// Numeric mewakili tipe yang mendukung operasi aritmetika.
// Ini mencakup semua tipe integer dan floating-point.
type Numeric interface {
    int | int8 | int16 | int32 | int64 |
    uint | uint8 | uint16 | uint32 | uint64 |
    float32 | float64
}

4. Pertimbangkan Kinerja

Generik dikompilasi menjadi tipe konkret, jadi tidak ada overhead runtime. Namun, waspadai ukuran kode jika Anda menginstansiasi banyak kombinasi tipe:

// Setiap kombinasi menciptakan kode terpisah
var intStack = NewStack[int]()
var stringStack = NewStack[string]()
var floatStack = NewStack[float64]()

Aplikasi Dunia Nyata

Pembangun Query Database

Generik sangat berguna ketika membangun pembangun query database atau bekerja dengan ORM. Ketika bekerja dengan database dalam Go, Anda mungkin menemukan perbandingan kami tentang Go ORMs untuk PostgreSQL membantu memahami bagaimana generik dapat meningkatkan keamanan tipe dalam operasi database.

type QueryBuilder[T any] struct {
    table string
    where []string
}

func (qb *QueryBuilder[T]) Where(condition string) *QueryBuilder[T] {
    qb.where = append(qb.where, condition)
    return qb
}

func (qb *QueryBuilder[T]) Find() ([]T, error) {
    // Implementasi
    return nil, nil
}

Manajemen Konfigurasi

Ketika membangun aplikasi CLI atau sistem konfigurasi, generik dapat membantu menciptakan loader konfigurasi yang aman secara tipe. Jika Anda membangun alat baris perintah, panduan kami tentang membangun aplikasi CLI dengan Cobra & Viper menunjukkan bagaimana generik dapat meningkatkan penanganan konfigurasi.

type ConfigLoader[T any] struct {
    path string
}

func (cl *ConfigLoader[T]) Load() (T, error) {
    var config T
    // Muat dan unmarshal konfigurasi
    return config, nil
}

Perpustakaan Utilitas

Generik bersinar ketika menciptakan perpustakaan utilitas yang bekerja dengan berbagai tipe. Misalnya, ketika menghasilkan laporan atau bekerja dengan format data berbeda, generik dapat memberikan keamanan tipe. Artikel kami tentang menghasilkan laporan PDF dalam Go menunjukkan bagaimana generik dapat diterapkan ke utilitas pembuatan laporan.

Kode Kritis Kinerja

Dalam aplikasi sensitif kinerja seperti fungsi serverless, generik dapat membantu mempertahankan keamanan tipe tanpa overhead runtime. Ketika mempertimbangkan pilihan bahasa untuk aplikasi serverless, memahami karakteristik kinerja adalah penting. Analisis kami tentang kinerja AWS Lambda di seluruh JavaScript, Python, dan Golang menunjukkan bagaimana kinerja Go, dikombinasikan dengan generik, dapat menjadi keuntungan.

Kesalahan Umum

1. Mengoverkendalikan Tipe

Hindari membuat batasan terlalu ketat ketika tidak perlu:

// Terlalu ketat
func Process[T int | string](items []T) { }

// Lebih baik - lebih fleksibel
func Process[T comparable](items []T) { }

2. Mengabaikan Inferensi Tipe

Biarkan Go menginferensikan tipe ketika mungkin:

// Tipe eksplisit yang tidak diperlukan
result := Max[int](10, 20)

// Lebih baik - biarkan Go menginferensikannya
result := Max(10, 20)

3. Lupa Nilai Nol

Ingat bahwa tipe generik memiliki nilai nol:

func Get[T any](slice []T, index int) (T, bool) {
    if index < 0 || index >= len(slice) {
        var zero T  // Penting: kembalikan nilai nol
        return zero, false
    }
    return slice[index], true
}

Kesimpulan

Generik dalam Go menyediakan cara yang kuat untuk menulis kode yang aman secara tipe dan dapat digunakan kembali tanpa mengorbankan kinerja atau kebacaan. Dengan memahami sintaks, batasan, dan pola umum, Anda dapat memanfaatkan generik untuk mengurangi duplikasi kode dan meningkatkan keamanan tipe dalam aplikasi Go Anda.

Ingatlah untuk menggunakan generik secara bijak—tidak setiap situasi membutuhkannya. Ketika ragu, pilih solusi yang lebih sederhana seperti antarmuka atau tipe konkret. Namun, ketika Anda perlu bekerja dengan berbagai tipe sambil mempertahankan keamanan tipe, generik adalah alat yang sangat baik dalam toolkit Go Anda.

Seiring Go terus berkembang, generik menjadi fitur penting untuk membangun aplikasi modern yang aman secara tipe. Baik Anda membangun struktur data, perpustakaan utilitas, atau abstraksi kompleks, generik dapat membantu Anda menulis kode yang lebih bersih dan lebih mudah dipelihara.

Tautan Berguna dan Artikel Terkait