Go'da Generics (Jenerikler) — Kapsamlı Bir Rehber

17 Ağustos 2026 · netologist · 16 dakika, 3299 kelime ·

İçindekiler

  1. Giriş
  2. Tarihçe ve Motivasyon
  3. Temel Sözdizimi
  4. Tip Parametreleri (Type Parameters)
  5. Tip Kısıtlamaları (Type Constraints)
  6. constraints ve cmp Paketleri
  7. Tip Kümeleri ve Kısıtlama Olarak Arayüzler
  8. Generic Fonksiyonlar
  9. Generic Tipler (Struct’lar)
  10. Generic Metotlar ve Sınırlamaları
  11. Tip Çıkarımı (Type Inference)
  12. Instantiation (Somutlaştırma)
  13. any Tipi
  14. comparable Kısıtlaması
  15. Union Elemanları ve ~ (Alttaki Tip) Operatörü
  16. Generic Veri Yapıları
  17. Standart Kütüphanedeki Generic Paketler
  18. Değişken Sayıda Generic ve Birden Fazla Tip Parametresi
  19. Performans Değerlendirmeleri
  20. Sık Yapılan Hatalar
  21. En İyi Uygulamalar
  22. Generics vs. interface{} / any vs. Kod Üretimi
  23. Gerçek Dünya Örnekleri
  24. 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ı:

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ı:

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:

Ç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:

  1. Bir isim (geleneksel olarak T, K, V, E gibi tek büyük harf)
  2. 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ı:


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:

  1. Önceden tanımlı: any (interface{}‘in takma adı/alias’ı), comparable
  2. Metot kümesine sahip arayüzler aracılığıyla (normal Go arayüzleri gibi)
  3. 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:

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:

  1. Fonksiyon argümanı tip çıkarımı: Normal fonksiyon argümanlarının tiplerinden tip parametrelerini çıkarır.
  2. Kısıtlama tip çıkarımı: Bir tip parametresini, başka bir tip parametresinin yapısal kısıtlamasından çıkarır (örneğin, ~[]E gibi 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:

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:

// 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:

Önemli performans çıkarımları:


Sık Yapılan Hatalar

  1. var zero T kullanmadan T‘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]
    }
    
  2. Metotların yeni tip parametreleri ekleyemeyeceğini unutmak (yukarıdaki Generic Metotlar bölümüne bakınız).

  3. 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ğerleri comparable altında yine de runtime’da panic’e yol açabilir.

  4. Basit bir interface{}/any veya 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.

  5. Kısıtlamalarda ~ işaretini unutmak, bu sessizce aynı alttaki tipi paylaşan özel/tanımlanmış tipleri dışarıda bırakır.

  6. 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.

  7. Metot değerleriyle generic tipleri yanlış şekilde somutlaştırmaya çalışmakStack[int]{}.Push için işaretçi (pointer) alıcı bağlamının doğru şekilde ayarlanması gerektiğini unutmayın.


En İyi Uygulamalar


Generics vs. interface{} / any vs. Kod Üretimi

ÖzellikGenericsinterface{}/anyKod Ü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ğiYerel dil özelliğiEkstra derleme adımı/araç gerektirir
Okunabilirlikİyi, aşırıya kaçılmazsaBasit ama daha az güvenliAyrı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.