Go'da Generics (Jenerikler) — Kapsamlı Bir Rehber
İçindekiler
- Giriş
- Tarihçe ve Motivasyon
- Temel Sözdizimi
- Tip Parametreleri (Type Parameters)
- Tip Kısıtlamaları (Type Constraints)
constraintsvecmpPaketleri- Tip Kümeleri ve Kısıtlama Olarak Arayüzler
- Generic Fonksiyonlar
- Generic Tipler (Struct’lar)
- Generic Metotlar ve Sınırlamaları
- Tip Çıkarımı (Type Inference)
- Instantiation (Somutlaştırma)
anyTipicomparableKısıtlaması- Union Elemanları ve
~(Alttaki Tip) Operatörü - Generic Veri Yapıları
- Standart Kütüphanedeki Generic Paketler
- Değişken Sayıda Generic ve Birden Fazla Tip Parametresi
- Performans Değerlendirmeleri
- Sık Yapılan Hatalar
- En İyi Uygulamalar
- Generics vs.
interface{}/anyvs. Kod Üretimi - Gerçek Dünya Örnekleri
- Sonuç
Giriş
Generics, Go 1.18 sürümüyle (Mart 2022) dile eklendi ve bu, Go’nun 2012’deki 1.0 sürümünden bu yana yaptığı en büyük dil değişikliklerinden biri oldu. Generics, fonksiyonların ve tiplerin, interface{} (şimdi any) ve çalışma zamanı (runtime) tip doğrulamalarına başvurmadan, tam derleme zamanı (compile-time) tip güvenliğini koruyarak farklı tipler üzerinde çalışabilmesini sağlar.
Generics’ten önce Go geliştiricilerinin üç temel çözümü vardı:
- Her tip için ayrı fonksiyon yazmak (kod tekrarı)
interface{}ve tip doğrulamaları/switch’leri kullanmak (derleme zamanı güvenliğini kaybeder, runtime maliyeti ekler)go generategibi kod üretim araçlarıyla şablon (template) kullanmak (derleme sürecine karmaşıklık ekler)
Generics, tek bir tip parametreli (type-parameterized) implementasyon yazmanıza izin vererek bu sorunu çözer; bu implementasyon derleyici tarafından kontrol edilir ve (çoğu durumda) makul bir performans için derleme zamanında monomorfize edilir veya “dictionary passing” (sözlük geçirme) yöntemiyle işlenir.
Tarihçe ve Motivasyon
Go ekibi, generics tasarımını uzun yıllar boyunca araştırdı. Önemli kilometre taşları:
- 2010: Generics hakkındaki ilk tartışmalar, Go’nun kamuya açık sürümünden hemen sonra başladı.
- 2018–2019: “Tip parametreleri” taslak tasarımı yayımlandı ve reddedilen bir “contracts” (sözleşmeler) önerisi dahil birkaç iterasyondan geçti.
- Haziran 2021: Go 1.18 için nihai öneri kabul edildi.
- Mart 2022: Go 1.18, generics, tip parametreleri, kısıtlamalar (constraints) ve deneysel
slices/mapspaketleriyle (golang.org/x/exp/slices,golang.org/x/exp/maps) birlikte yayımlandı. - Go 1.21 (2023):
slices,maps,cmppaketleri vemin/maxyerleşik (builtin) fonksiyonları standart kütüphaneye taşındı.
Temel motivasyon, konteyner tipleri ve algoritmalar için kod tekrarını ortadan kaldırmaktı (örneğin generic bir Stack[T], Map(), Filter(), Reduce()), bunu yaparken Go’nun sadelik ve hızlı derleme felsefesini korumaktı.
Temel Sözdizimi
Generic bir fonksiyon, fonksiyon adının hemen ardından köşeli parantez içinde tip parametreleri bildirir:
func Max[T constraints.Ordered](a, b T) T {
if a > b {
return a
}
return b
}
Bunu parçalara ayıralım:
[T constraints.Ordered]—Tadında bir tip parametresi bildirir; bu parametreconstraints.Orderedkısıtlamasını sağlayan tiplerle sınırlıdır (yani<,>gibi operatörleri destekler)(a, b T)— fonksiyon parametrelerinin ikisi deTtipindedir- Dönüş tipi de
T‘dir
Çağırma şekli:
result := Max[int](3, 7) // açık (explicit) instantiation
result2 := Max(3, 7) // örtük (implicit) — derleyici T = int çıkarımı yapar
Tip Parametreleri (Type Parameters)
Tip parametreleri, normal parametreler gibi davranır ancak değer yerine tipleri temsil ederler. Köşeli parantez içinde bildirilirler ve her tip parametresinin şunları vardır:
- Bir isim (geleneksel olarak
T,K,V,Egibi tek büyük harf) - Yerine hangi tiplerin geçebileceğini sınırlayan bir kısıtlama (constraint)
func Print[T any](s []T) {
for _, v := range s {
fmt.Println(v)
}
}
Birden fazla tip parametresi virgülle ayrılır:
func MapKeys[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
}
Go ekosisteminde yaygın olarak kullanılan isimlendirme kuralları:
T— genel bir “Type” (Tip)K,V— Key (Anahtar) ve Value (Değer) (map’ler için)E— Element (Eleman) (slice’lar için)S— bir Slice tip parametresi (örn.S ~[]E)
Tip Kısıtlamaları (Type Constraints)
Bir kısıtlama, izin verilen tip argümanları kümesini ve bu tipteki değerlerin desteklediği işlemler kümesini belirten bir arayüz (interface) tipidir. Kısıtlamalar şu şekillerde tanımlanabilir:
- Önceden tanımlı:
any(interface{}‘in takma adı/alias’ı),comparable - Metot kümesine sahip arayüzler aracılığıyla (normal Go arayüzleri gibi)
- Tip kümesine sahip arayüzler aracılığıyla (union
|ve alttaki-tip~elemanları kullanılarak)
type Number interface {
int | int8 | int16 | int32 | int64 |
float32 | float64
}
func Sum[T Number](nums []T) T {
var total T
for _, n := range nums {
total += n
}
return total
}
Kısıtlamalar ayrıca metot gerektirebilir:
type Stringer interface {
String() string
}
func Join[T Stringer](items []T, sep string) string {
parts := make([]string, len(items))
for i, item := range items {
parts[i] = item.String()
}
return strings.Join(parts, sep)
}
Kısıtlamalar hem metot kümesini hem de tip kümesini birleştirebilir:
type OrderedStringer interface {
~int | ~string
String() string
}
constraints ve cmp Paketleri
Başlangıçta, deneysel golang.org/x/exp/constraints paketi Ordered, Signed, Unsigned, Integer, Float ve Complex gibi yaygın kısıtlamaları sağlıyordu.
Go 1.21 itibarıyla, standart kütüphanedeki cmp paketi şunları sağlar:
package cmp
type Ordered interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
~float32 | ~float64 | ~string
}
func Compare[T Ordered](x, y T) int
func Less[T Ordered](x, y T) bool
Kullanım örneği:
import "cmp"
func Max[T cmp.Ordered](a, b T) T {
if a > b {
return a
}
return b
}
Go 1.21’den itibaren, min ve max yerleşik (builtin) fonksiyonları, generics’e hiç ihtiyaç duymadan sıralanabilir (ordered) tipler için doğrudan kullanılabilir hale geldi:
x := min(3, 7) // 3
y := max(3.1, 7.2) // 7.2
Tip Kümeleri ve Kısıtlama Olarak Arayüzler
Go 1.18’den itibaren, arayüzler sadece metot kümelerini değil, tip kümelerini de tanımlayabilir. Salt kısıtlama amacıyla kullanılan bir arayüz şunları listeleyebilir:
- Union (birleşim) elemanları:
int | int64 | float64— tip argümanı bunlardan biri olmalıdır - Alttaki tip (underlying type) elemanları:
~int— tip argümanının alttaki tipiintolmalıdır (bu,type MyInt intgibi tanımlanmış (defined) tiplere de izin verir)
type Integer interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr
}
~ olmadan, kısıtlamayı yalnızca tam olarak int tipi sağlar — type UserID int gibi özel bir tip doğrudan int ile eşleşmez, ancak ~int ile eşleşir.
Bu ayrım pratikte oldukça önemlidir:
type Celsius float64
type Number interface {
int | float64 // ~ yok
}
func Double[T Number](x T) T { return x * 2 }
// Double(Celsius(10)) // DERLEME HATASI: Celsius, Number'ı implemente etmiyor
karşı:
type Number interface {
~int | ~float64 // ~ ile
}
func Double[T Number](x T) T { return x * 2 }
Double(Celsius(10)) // Sorunsuz çalışır
Generic Fonksiyonlar
Generic fonksiyonlar en yaygın kullanım senaryosudur. Birkaç örnek:
Filter (Filtreleme):
func Filter[T any](s []T, keep func(T) bool) []T {
result := make([]T, 0, len(s))
for _, v := range s {
if keep(v) {
result = append(result, v)
}
}
return result
}
Map (Dönüştürme):
func Map[T, U any](s []T, f func(T) U) []U {
result := make([]U, len(s))
for i, v := range s {
result[i] = f(v)
}
return result
}
Reduce (İndirgeme):
func Reduce[T, U any](s []T, init U, f func(U, T) U) U {
acc := init
for _, v := range s {
acc = f(acc, v)
}
return acc
}
Kullanım:
nums := []int{1, 2, 3, 4, 5}
evens := Filter(nums, func(n int) bool { return n%2 == 0 })
squares := Map(nums, func(n int) int { return n * n })
sum := Reduce(nums, 0, func(acc, n int) int { return acc + n })
Generic Tipler (Struct’lar)
Struct’lar, arayüzler ve diğer tip bildirimleri de parametreli hale getirilebilir:
type Pair[K, V any] struct {
Key K
Value V
}
func NewPair[K, V any](key K, value V) Pair[K, V] {
return Pair[K, V]{Key: key, Value: value}
}
Generic tipler iç içe geçirilebilir ve birleştirilebilir:
type Tree[T any] struct {
Value T
Children []*Tree[T]
}
type Graph[T comparable] struct {
Nodes map[T]bool
Edges map[T][]T
}
Generic bir tip kullanırken, tip argümanlarını belirtmeniz gerekir (yukarıdaki NewPair örneğinde olduğu gibi bir kurucu fonksiyondan çıkarılabildikleri durumlar hariç):
p := Pair[string, int]{Key: "age", Value: 30}
Generic Metotlar ve Sınırlamaları
Generic tipler üzerindeki metotlar, alıcının (receiver) tip parametrelerini otomatik olarak devralır:
type Stack[T any] struct {
items []T
}
func (s *Stack[T]) Push(v T) {
s.items = append(s.items, v)
}
func (s *Stack[T]) Pop() (T, bool) {
var zero T
if len(s.items) == 0 {
return zero, false
}
last := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return last, true
}
Önemli sınırlama: Go, bir metodun kendisinin, alıcısının tip parametrelerinin ötesinde yeni tip parametreleri tanıtmasına izin vermez. Bu, kendi bağımsız tip parametresine sahip generic metotlar yazamayacağınız anlamına gelir (Java veya C# gibi dillere alışkın olanlar için sıkça karşılaşılan bir kısıtlamadır):
type Container[T any] struct {
value T
}
// Buna İZİN VERİLMEZ:
// func (c Container[T]) MapTo[U any](f func(T) U) Container[U] { ... }
Çözüm yolu, bunun yerine bağımsız bir generic fonksiyon kullanmaktır:
func MapContainer[T, U any](c Container[T], f func(T) U) Container[U] {
return Container[U]{value: f(c.value)}
}
Bu kısıtlama, metotların kendi tip parametrelerine sahip olabilmesi durumunda Go’nun metot kümesi ve arayüz karşılama (interface satisfaction) kurallarının önemli ölçüde karmaşıklaşmasından kaynaklanır — bu, dilin gelecekteki olası evrimi için aktif bir tartışma konusudur ancak bugün desteklenmemektedir.
Tip Çıkarımı (Type Inference)
Go derleyicisi çoğu zaman tip argümanlarını çıkarabilir, böylece bunları açıkça belirtmeniz gerekmez. İki ana çıkarım mekanizması vardır:
- Fonksiyon argümanı tip çıkarımı: Normal fonksiyon argümanlarının tiplerinden tip parametrelerini çıkarır.
- Kısıtlama tip çıkarımı: Bir tip parametresini, başka bir tip parametresinin yapısal kısıtlamasından çıkarır (örneğin,
~[]Egibi bir slice tipi kısıtlamasından eleman tipini çıkarmak).
func First[T any](s []T) T {
return s[0]
}
x := First([]int{1, 2, 3}) // T, int olarak çıkarılır, First[int](...) yazmaya gerek yok
Çıkarımın sınırları vardır — yalnızca bir fonksiyonun dönüş tipinden tip çıkaramaz ve çıkarım, değişken atama hedef tipi gibi bağlamdan “geriye doğru akmaz”. Çıkarım başarısız olduğunda, tip argümanlarını açıkça belirtmeniz gerekir:
var f float64 = 3.0
// n := Convert(f) // eğer Convert[T, U any](x T) U ise, U çıkarılamaz!
n := Convert[float64, int](f) // açıkça belirtilmelidir
Instantiation (Somutlaştırma)
“Instantiation”, tip parametreleri yerine somut tipler koyma eylemidir ve generic olmayan bir fonksiyon veya tip üretir:
type List[T any] []T
var IntList = List[int]{1, 2, 3} // somutlaştırılmış tip
var StringList = List[string]{"a", "b"} // farklı somutlaştırılmış tip
Fonksiyon değerleri de hemen çağrılmadan somutlaştırılabilir:
var maxInt = Max[int] // maxInt, func(int, int) int tipindedir
Kaynak kodda tek bir generic tanımı paylaşsalar bile, her benzersiz instantiation, tip sistemi tarafından ayrı bir tip/fonksiyon olarak ele alınır.
any Tipi
any, generics ile birlikte okunabilirlik için tanıtılan, interface{} için önceden tanımlı basit bir takma addır (alias):
type any = interface{}
any, “kısıtlama yok — herhangi bir tipe izin verilir” anlamında bir kısıtlama olarak yaygın şekilde kullanılır:
func Identity[T any](v T) T {
return v
}
any‘in kısıtlama olarak kullanımının, interface{}‘in normal (generic olmayan) bir parametre tipi olarak kullanımından farklı olduğuna dikkat edin — generics ile derleyici, her instantiation için tek bir somut tipin tutarlı bir şekilde kullanıldığını yine de zorunlu kılar ve bu tipi bir tip doğrulamasına (type assertion) gerek kalmadan geri alırsınız.
comparable Kısıtlaması
comparable, == ve != operatörlerini destekleyen herhangi bir tip tarafından sağlanan, önceden tanımlı bir kısıtlamadır. Buna şunlar dahildir:
- Temel tipler: sayılar, string’ler, boolean’lar
- İşaretçiler (pointer)
- Kanallar (channel)
- Karşılaştırılabilir tiplerin dizileri (array)
- Tüm alanları karşılaştırılabilir olan struct’lar
Slice’ları, map’leri ve fonksiyonları hariç tutar (Go’da bunlar karşılaştırılabilir değildir).
func Contains[T comparable](s []T, target T) bool {
for _, v := range s {
if v == target {
return true
}
}
return false
}
Go 1.20’den itibaren, comparable (biraz tartışmalı bir şekilde) karşılaştırması runtime’da panic’e yol açabilecek tiplere de izin verir (karşılaştırılamaz dinamik değerler tutan interface’ler gibi). Bu nedenle comparable, her köşe durumda %100 panic’e karşı güvenli bir garanti değildir — ancak tipik kullanımda map anahtarları, == gibi durumlar için beklendiği gibi çalışır.
Union Elemanları ve ~ (Alttaki Tip) Operatörü
Bir kısıtlama arayüzündeki union elemanı, | ile ayrılmış alternatif tipleri listeler:
type Numeric interface {
int | int32 | int64 | float32 | float64
}
Bir tip argümanı, listedeki terimlerden herhangi birine özdeşse (veya ~ ile birleştirildiğinde, aynı alttaki tipe sahipse) union elemanını sağlar.
Union elemanlarındaki kısıtlamalar:
- Bir union’daki terimler örtüşemez (aynı union içinde hem
inthem~intolamaz, çünkü ikincisi zaten birincisini kapsar) - Bazı durumlarda, birden fazla terime sahip bir union içine metotlu arayüzler dahil edilemez
~olmadan tek terimli bir union elemanı, normal bir tip kısıtlaması gibi davranır
// Geçerli:
type Ordered interface {
~int | ~float64 | ~string
}
// Geçersiz — örtüşen terimler:
// type Bad interface {
// int | ~int
// }
Generic Veri Yapıları
Generics, yeniden kullanılabilir konteynerler ve algoritmalar geliştirilirken en çok fayda sağlar. Yaygın örnekler:
Bağlı Liste (Linked List)
type Node[T any] struct {
Value T
Next *Node[T]
}
type LinkedList[T any] struct {
Head *Node[T]
Tail *Node[T]
Len int
}
func (l *LinkedList[T]) Append(v T) {
n := &Node[T]{Value: v}
if l.Head == nil {
l.Head = n
l.Tail = n
} else {
l.Tail.Next = n
l.Tail = n
}
l.Len++
}
İkili Arama Ağacı (Binary Search Tree)
type BSTNode[T cmp.Ordered] struct {
Value T
Left, Right *BSTNode[T]
}
func (n *BSTNode[T]) Insert(v T) *BSTNode[T] {
if n == nil {
return &BSTNode[T]{Value: v}
}
if v < n.Value {
n.Left = n.Left.Insert(v)
} else if v > n.Value {
n.Right = n.Right.Insert(v)
}
return n
}
Generic Küme (Set)
type Set[T comparable] struct {
m map[T]struct{}
}
func NewSet[T comparable](items ...T) *Set[T] {
s := &Set[T]{m: make(map[T]struct{})}
for _, item := range items {
s.Add(item)
}
return s
}
func (s *Set[T]) Add(v T) { s.m[v] = struct{}{} }
func (s *Set[T]) Has(v T) bool { _, ok := s.m[v]; return ok }
func (s *Set[T]) Remove(v T) { delete(s.m, v) }
func (s *Set[T]) Len() int { return len(s.m) }
Standart Kütüphanedeki Generic Paketler
Go 1.21+ birkaç generic paket sunar:
slices
import "slices"
slices.Sort(s)
slices.Contains(s, x)
slices.Index(s, x)
slices.Reverse(s)
slices.Max(s)
slices.Min(s)
slices.Equal(s1, s2)
slices.Clone(s)
slices.Compact(s)
slices.Insert(s, i, v...)
slices.Delete(s, i, j)
slices.BinarySearch(s, x)
maps
import "maps"
maps.Keys(m) // bir iterator döner (Go 1.23+) veya eski sürümlerde slice
maps.Values(m)
maps.Equal(m1, m2)
maps.Clone(m)
maps.DeleteFunc(m, f)
maps.Copy(dst, src)
cmp
import "cmp"
cmp.Compare(a, b)
cmp.Less(a, b)
cmp.Or(a, b, c) // ilk sıfır olmayan değeri döner
iter (Go 1.23+)
Range-over-func için kullanılan Seq[T] ve Seq2[K, V] iterator fonksiyon tiplerini tanıtır ve generic iterator’lara olanak sağlar:
func Seq[T any](s []T) iter.Seq[T] {
return func(yield func(T) bool) {
for _, v := range s {
if !yield(v) {
return
}
}
}
}
Değişken Sayıda Generic ve Birden Fazla Tip Parametresi
Go, bazı dillerdeki değişken sayıda şablon (variadic template) özelliğinin aksine, gerçek anlamda “değişken sayıda tip parametresini” (keyfi sayıda tip parametresi) desteklemez. Ancak generics’i değişken sayıda değer parametresiyle birleştirebilirsiniz:
func NewSlice[T any](items ...T) []T {
return items
}
Bağımsız kısıtlamalara sahip birden fazla tip parametresi desteklenir ve yaygın olarak kullanılır:
func Zip[A, B any](as []A, bs []B) []struct {
First A
Second B
} {
n := min(len(as), len(bs))
result := make([]struct {
First A
Second B
}, n)
for i := 0; i < n; i++ {
result[i] = struct {
First A
Second B
}{as[i], bs[i]}
}
return result
}
Performans Değerlendirmeleri
Go’nun generics implementasyonu, GC shape stenciling (GC şekli kalıplama) adı verilen bir yöntemi dictionary passing (sözlük geçirme) ile birleştiren hibrit bir strateji kullanır:
- Aynı “GC şekline” (aynı boyut, aynı işaretçi düzeni) sahip tipler, tek bir derlenmiş implementasyonu paylaşabilir; tip’e özgü işlemleri (örneğin doğru karşılaştırma fonksiyonunu çağırmak) yönetmek için runtime’da bir “dictionary” (sözlük) geçirilir.
- Bu yaklaşım, ikili (binary) dosya boyutunu, tam monomorfizasyona (Rust veya C++ şablonlarında olduğu gibi, her tip için ayrı bir kod kopyası üretilmesine) göre daha makul tutarken, çoğu durumda
interface{}tabanlı dinamik dispatch’ten daha hızlı olmayı sürdürür.
Önemli performans çıkarımları:
- Generic kod, genellikle
interface{}tabanlı eşdeğerlerinden daha hızlıdır, çünkü boxing’i (pointer olmayan tipleri tutan interface değerleri için heap tahsisi) ve runtime’da tip doğrulamalarını (type assertion) önler. - Generic kod, şekil-paylaşımlı (shape-sharing) durumlarda dictionary-passing maliyeti nedeniyle genellikle tamamen monomorfize edilmiş, elle yazılmış tipe-özgü koddan biraz daha yavaştır — ancak derleyici ekibi her Go sürümünde optimizasyon yaptıkça bu fark önemli ölçüde daralmıştır.
- Varsayımda bulunmak yerine, kendi özel durumunuzda gerçek performansı doğrulamak için her zaman benchmark’lar (
testing.B) kullanılmalıdır. - Go 1.18’den bu yana, derleyicide sürekli iyileştirmeler olmuştur (örneğin generic fonksiyonların daha iyi inline edilmesi), bu nedenle daha yeni Go sürümleri genellikle 1.18’in kendisinden daha iyi generics performansına sahiptir.
Sık Yapılan Hatalar
var zero TkullanmadanT‘yi sıfır değer olarak kullanmaya çalışmak:func First[T any](s []T) T { if len(s) == 0 { return nil // T nil olamayan bir tip olabileceğinden DERLEME HATASI } return s[0] } // Doğrusu: func First[T any](s []T) T { var zero T if len(s) == 0 { return zero } return s[0] }Metotların yeni tip parametreleri ekleyemeyeceğini unutmak (yukarıdaki Generic Metotlar bölümüne bakınız).
comparable‘ın “her zaman==kullanımı güvenlidir” anlamına geldiğini varsaymak — Go 1.20’deki gevşetme sonrasında, karşılaştırılamaz dinamik tipler tutan interface değerlericomparablealtında yine de runtime’da panic’e yol açabilir.Basit bir
interface{}/anyveya hatta somut bir tipin daha net ve yeterli olacağı yerlerde generics’i aşırı kullanmak — Go’nun felsefesi okunabilirliği önceliklendirir ve her fonksiyonun generic olması gerekmez.Kısıtlamalarda
~işaretini unutmak, bu sessizce aynı alttaki tipi paylaşan özel/tanımlanmış tipleri dışarıda bırakır.Tip kısıtlamalarını tip doğrulamalarıyla (type assertion) karıştırmak — kısıtlamalar yalnızca derleme zamanı içindir; bir arayüz metot kümesinin yaptığı gibi runtime’da dinamik davranış dağıtımına (dispatch) izin vermezler.
Metot değerleriyle generic tipleri yanlış şekilde somutlaştırmaya çalışmak —
Stack[int]{}.Pushiçin işaretçi (pointer) alıcı bağlamının doğru şekilde ayarlanması gerektiğini unutmayın.
En İyi Uygulamalar
- Zamanından önce generic hale getirmeyin. Somut tiplerle başlayın; en az iki veya üç tip arasında gerçek bir tekrar gördüğünüzde generic bir sürüm çıkarın.
- El yapımı eşdeğerler yerine standart kütüphanenin generic paketlerini tercih edin (
slices,maps,cmp). - Anlamlı ama geleneksel tip parametresi isimleri kullanın (basit durumlar için
T,K,V,E; netliğin daha önemli olduğu karmaşık, çok parametreli generic kod için daha uzun isimler). - Kısıtlamaları gerektiği kadar dar, ama daha dar değil tutun — yalnızca
anyyeterliysecomparablegerektirmeyin. - Sıfır değerlerle ilgili davranışı açıkça belgeleyin, çünkü
nilher zamanTiçin geçerli değildir. - Performans eşitliği varsaymadan önce benchmark yapın, elle yazılmış özelleştirilmiş koda kıyasla.
- Derinlemesine iç içe geçmiş generic tiplerden kaçının (
Map[string, List[Pair[int, Set[string]]]]gibi), çünkü bunlar okunabilirliği zedeler — imzaları basitleştirmek için adlandırılmış tip takma adları (type alias) kullanmayı düşünün.
Generics vs. interface{} / any vs. Kod Üretimi
| Özellik | Generics | interface{}/any | Kod Üretimi |
|---|---|---|---|
| Derleme zamanı tip güvenliği | ✅ Tam | ❌ Yok (runtime doğrulama gerekir) | ✅ Tam |
| Performans | İyi (dictionary passing) | Daha yavaş (boxing, dinamik dispatch) | En iyi (tamamen özelleştirilmiş) |
| Kod tekrarı | Yok (tek generic tanım) | Yok, ama tip güvenliği kaybolur | Üretilen kod tekrarlanır |
| Derleme karmaşıklığı | Yerel dil özelliği | Yerel dil özelliği | Ekstra derleme adımı/araç gerektirir |
| Okunabilirlik | İyi, aşırıya kaçılmazsa | Basit ama daha az güvenli | Ayrıntılı olabilir, üretilen dosyalar |
Gerçek Dünya Örnekleri
Generic Result/Option Tipi
type Result[T any] struct {
Value T
Err error
}
func Ok[T any](v T) Result[T] {
return Result[T]{Value: v}
}
func Err[T any](err error) Result[T] {
return Result[T]{Err: err}
}
func (r Result[T]) IsOk() bool {
return r.Err == nil
}
Generic Cache (Önbellek)
type Cache[K comparable, V any] struct {
mu sync.RWMutex
items map[K]V
}
func NewCache[K comparable, V any]() *Cache[K, V] {
return &Cache[K, V]{items: make(map[K]V)}
}
func (c *Cache[K, V]) Get(key K) (V, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
v, ok := c.items[key]
return v, ok
}
func (c *Cache[K, V]) Set(key K, value V) {
c.mu.Lock()
defer c.mu.Unlock()
c.items[key] = value
}
Generic Pipeline / Fonksiyonel Kompozisyon
func Compose[A, B, C any](f func(A) B, g func(B) C) func(A) C {
return func(a A) C {
return g(f(a))
}
}
double := func(x int) int { return x * 2 }
toString := func(x int) string { return fmt.Sprintf("%d", x) }
doubleThenString := Compose(double, toString)
fmt.Println(doubleThenString(5)) // "10"
Sonuç
Go 1.18’de tanıtılan ve Go 1.21–1.23+ sürümleri boyunca istikrarlı bir şekilde geliştirilen Go generics, dilin sadelik ve performans konusundaki temel değerlerinden ödün vermeden tip güvenli, yeniden kullanılabilir soyutlamalar getirir. Özellik seti, Rust, C++ veya Java/C# gibi dillere kıyasla kasıtlı olarak daha muhafazakar olsa da (değişken sayıda tip parametresi yok, metotlarda yeni tip parametresi yok, sınırlı özelleştirme), pratik kullanım senaryolarının büyük çoğunluğunu kapsar: generic konteynerler, algoritmalar ve yardımcı fonksiyonlar.
Go’da generics’i iyi kullanmanın anahtarı ölçülülüktür (restraint): gerçekten tekrarı ortadan kaldırdıkları veya yeniden kullanılabilir, tip güvenli soyutlamalar açığa çıkardıkları yerlerde kullanın — ve standart kütüphanenin slices, maps ve cmp paketleri sorununuzu zaten çözüyorsa bunları tercih edin.