Generik Go: Kasus Penggunaan dan Pola
Kode yang aman secara tipis dan dapat digunakan kembali dengan Go generics
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.

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 denganinterface{})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
- Tutorial Generik Go - Blog Resmi Go
- Proposal Parameter Tipe
- Dokumentasi Generik Go
- Go Efektif - Generik
- Catatan Rilis Go 1.18 - Generik
- Spesifikasi Bahasa Go - Parameter Tipe
- Playground Generik Go
- Kapan Menggunakan Generik - Blog Go
- Go Cheatsheet
- Membangun Aplikasi CLI dalam Go dengan Cobra & Viper
- Perbandingan Go ORMs untuk PostgreSQL: GORM vs Ent vs Bun vs sqlc
- Menghasilkan PDF dalam GO - Perpustakaan dan contoh
- Kinerja AWS Lambda: JavaScript vs Python vs Golang