Go'da generics, fonksiyonlara ve tiplere köşeli parantez içinde tip parametreleri eklenerek yazılır: func Map[T, U any](s []T, f func(T) U) []U ya da type Yigin[T any] struct{...}. Her tip parametresinin bir kısıtı (constraint) vardır ve bu kısıt hangi tiplerin kullanılabileceğini belirler. Fonksiyonu çağırırken tip argümanını Map[int, string](...) biçiminde açıkça verebilir ya da çoğu durumda derleyicinin argümanlardan çıkarmasına (type inference) bırakabilirsiniz. Generics Go'ya 1.18 sürümüyle geldi.

Bu yazıda resmî generics eğitimindeki örneği, generic fonksiyon ve generic tip yazmayı, örneklemeyi (instantiation), tip çıkarımının çalıştığı ve çalışmadığı durumları, generic tiplere metot eklemeyi ve generics'in ne zaman doğru araç olduğunu Go 1.27.3 ile çalıştırılmış örneklerle anlatıyoruz. Kısıtların ayrıntısı (~T, tip birleşimleri, comparable, cmp.Ordered) generics kısıtları yazısında ele alınıyor. Serinin tamamı için Go öğrenme rehberi sayfasına bakabilirsiniz.

Neden generics?

Neden generics?

Go FAQ, tip parametrelerinin amacını şöyle özetler: fonksiyonlar ve veri yapıları, kullanılacakları an belirlenecek tipler üzerinden tanımlanabilir. Örneğin sıralanabilir herhangi bir tipin iki değerinden küçüğünü veren tek bir fonksiyon yazabilirsiniz; her tip için ayrı sürüm yazmanız gerekmez. Generics'ten önce bunun iki yolu vardı: kodu her tip için kopyalamak ya da any ve tip iddialarıyla çalışmak. İlki tekrar, ikincisi tip güvenliği kaybı demekti. Go Blog'daki giriş yazısı, generics'in dile üç şey kattığını söyler: fonksiyon ve tipler için tip parametreleri, arayüzlerin tip kümesi olarak tanımlanması ve tip çıkarımı.

İlk generic fonksiyon: resmî eğitimdeki örnek

Resmî "Getting started with generics" eğitimi, biri int64 biri float64 değerli iki map'in değerlerini toplayan iki ayrı fonksiyonla başlar ve sonra bunları tek bir generic fonksiyonda birleştirir. Aşağıdaki program aynı akışı izliyor:

go
package main

import "fmt"

func SumInts(m map[string]int64) int64 {
	var s int64
	for _, v := range m {
		s += v
	}
	return s
}

func SumFloats(m map[string]float64) float64 {
	var s float64
	for _, v := range m {
		s += v
	}
	return s
}

// Tek bir generic fonksiyon iki gövdenin yerini alır.
func SumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V {
	var s V
	for _, v := range m {
		s += v
	}
	return s
}

func main() {
	ints := map[string]int64{"birinci": 34, "ikinci": 12}
	floats := map[string]float64{"birinci": 35.98, "ikinci": 26.99}

	fmt.Println(SumInts(ints), SumFloats(floats))
	fmt.Println(SumIntsOrFloats[string, int64](ints),
		SumIntsOrFloats[string, float64](floats))
	fmt.Println(SumIntsOrFloats(ints), SumIntsOrFloats(floats))
}

// Çıktı:
// 46 62.97
// 46 62.97
// 46 62.97

SumIntsOrFloats iki tip parametresi bildiriyor. K için kısıt comparable: eğitimin açıkladığı gibi map anahtarları == ve != ile karşılaştırılabilmelidir, bu kısıt tam bu durum için vardır. V için kısıt int64 | float64 birleşimidir, yani V bu iki tipten biri olabilir. Gövdede s += v yazabilmemizin nedeni, kısıttaki bütün tiplerin + işlemini desteklemesidir. İkinci satırda tip argümanlarını açıkça verdik, üçüncü satırda ise derleyici bunları map argümanının tipinden çıkardı.

Örnekleme: tip argümanı vermek

Generic bir fonksiyona tip argümanı vermeye örnekleme (instantiation) denir. Spesifikasyona göre örnekleme iki adımda olur: önce her tip argümanı generic bildirimdeki ilgili tip parametresinin yerine konur, sonra her tip argümanının kendi kısıtını sağlayıp sağlamadığı kontrol edilir. İkinci adım başarısız olursa program geçersizdir. Örnekleme sonucunda generic olmayan sıradan bir fonksiyon elde edilir; bu yüzden f := SumIntsOrFloats[string, int64] yazıp f'yi normal bir fonksiyon değeri gibi kullanabilirsiniz.

Kısmi tip argüman listesi de verilebilir, ama liste soldan başlamalıdır ve boş olamaz: spesifikasyonun ifadesiyle tip argümanları "sağdan sola" atlanabilir. Generic tipler için ise kural daha katıdır: bütün tip argümanları her zaman açıkça verilmelidir.

Dilimler üzerinde Map ve Filter

Dilimler üzerinde Map ve Filter

Go Blog'daki "When To Use Generics" yazısı, tip parametrelerinin en doğal kullanım alanının dilim, map ve kanal gibi dilin kendi kap (container) tipleri üzerinde çalışan ve eleman tipi hakkında özel bir varsayım yapmayan fonksiyonlar olduğunu söyler. Aşağıdaki Map ve Filter bu tanıma uyuyor:

go
package main

import (
	"fmt"
	"strings"
)

func Map[T, U any](s []T, f func(T) U) []U {
	r := make([]U, 0, len(s))
	for _, v := range s {
		r = append(r, f(v))
	}
	return r
}

func Filter[T any](s []T, tut func(T) bool) []T {
	var r []T
	for _, v := range s {
		if tut(v) {
			r = append(r, v)
		}
	}
	return r
}

func main() {
	sayilar := []int{1, 2, 3, 4, 5, 6}
	ciftler := Filter(sayilar, func(n int) bool { return n%2 == 0 })
	kareler := Map(ciftler, func(n int) int { return n * n })
	metin := Map(kareler, func(n int) string { return fmt.Sprint(n) })
	fmt.Println(ciftler, kareler, strings.Join(metin, "+"))

	uzunluk := Map([]string{"go", "generic", "tip"}, func(s string) int { return len(s) })
	fmt.Println(uzunluk)
}

// Çıktı:
// [2 4 6] [4 16 36] 4+16+36
// [2 7 3]

Hiçbir çağrıda tip argümanı yazmadık; T ve U dilim ve fonksiyon argümanlarının tiplerinden çıkarıldı. Map aynı satırda hem int'ten int'e hem int'ten string'e dönüşüm için kullanıldı. Kendi yardımcı fonksiyonlarınızı yazmadan önce standart kütüphaneye bakmanızı öneririz: slices ve maps paketleri sıralama, arama, karşılaştırma gibi birçok generic fonksiyonu hazır sunar (bkz. slices, maps ve sort paketleri yazısı).

Generic tipler ve metotları

Tip parametreleri yalnızca fonksiyonlarda değil, tip bildirimlerinde de kullanılabilir. Generic bir tipin metodunu yazarken alıcı, tipin parametrelerini yeniden bildirir; spesifikasyona göre bu bildirim bir örneklemeye benzer, parametre adlarının tip tanımındakilerle aynı olması gerekmez ve kısıtlar tip tanımından gelir. Aşağıda herhangi bir eleman tipiyle çalışan bir yığın (stack) ve iki tip parametreli bir çift yapısı var:

go
package main

import "fmt"

// Yigin, herhangi bir eleman tipiyle çalışan bir LIFO yapısıdır.
type Yigin[T any] struct {
	elemanlar []T
}

func (y *Yigin[T]) Ekle(v T) {
	y.elemanlar = append(y.elemanlar, v)
}

func (y *Yigin[T]) Cikar() (T, bool) {
	var sifir T
	if len(y.elemanlar) == 0 {
		return sifir, false
	}
	son := y.elemanlar[len(y.elemanlar)-1]
	y.elemanlar = y.elemanlar[:len(y.elemanlar)-1]
	return son, true
}

func (y *Yigin[T]) Uzunluk() int { return len(y.elemanlar) }

type Cift[K comparable, V any] struct {
	Anahtar K
	Deger   V
}

func main() {
	var y Yigin[string] // sıfır değeri kullanıma hazır
	y.Ekle("a")
	y.Ekle("b")
	v, ok := y.Cikar()
	fmt.Println(v, ok, y.Uzunluk())
	y.Cikar()
	v, ok = y.Cikar()
	fmt.Printf("%q %v\n", v, ok)

	sayilar := &Yigin[int]{}
	sayilar.Ekle(7)
	fmt.Printf("%T %T\n", y, sayilar)

	c := Cift[string, int]{"yaş", 30}
	fmt.Printf("%+v %T\n", c, c)
}

// Çıktı:
// b true 1
// "" false
// main.Yigin[string] *main.Yigin[int]
// {Anahtar:yaş Deger:30} main.Cift[string,int]

Üç ayrıntıya dikkat edin. var sifir T, tip parametresinin sıfır değerini elde etmenin standart yoludur; T ne olursa olsun geçerlidir. Boş yığından çıkarma, string'in sıfır değeri olan boş metni ve false'u döndürdü. %T çıktısı, örneklenmiş tipin adının tip argümanlarını da içerdiğini gösteriyor; FAQ'a göre Go'da generic bir tipin yansıtma (reflection) bilgisi derleme anındaki tam tip bilgisini taşır. Son olarak generic bir tipi örneklemeden kullanamazsınız: var y Yigin yazmak cannot use generic type Yigin[T any] without instantiation hatası verir.

Tip çıkarımı ne zaman çalışmaz?

Giriş yazısında anlatılan fonksiyon argümanından tip çıkarımı, yalnızca fonksiyon parametrelerinde kullanılan tip parametreleri için çalışır. Bir tip parametresi yalnızca dönüş değerinde ya da yalnızca gövdede geçiyorsa derleyici onu çıkaramaz:

go
package main

import "fmt"

func Sifir[T any]() T {
	var z T
	return z
}

func main() {
	a := Sifir()
	fmt.Println(a)
}
text
# command-line-arguments
./main.go:11:12: in call to Sifir, cannot infer T (declared at ./main.go:5:12)

Çözüm tip argümanını açıkça vermektir: Sifir[int]() 0, Sifir[string]() boş metin döndürür. Dönüş değerinin atandığı değişkenin tipi de yardımcı olmaz: var c float64 = Sifir() satırı Go 1.27.3'te aynı "cannot infer T" hatasını verir. FAQ, tip çıkarımının bilerek sınırlı tutulduğunu, amacın hangi tipin çıkarıldığı konusunda hiçbir zaman şüphe bırakmamak olduğunu açıklar; açık tip argümanı vermek her zaman mümkündür.

Çıkarımın ikinci türü kısıttan tip çıkarımıdır: bir tip parametresinin kısıtı başka bir tip parametresiyle yazılmışsa ([S ~[]E, E any]), birinin tipi bilindiğinde diğeri kısıttan bulunur. Bu desenin neden önemli olduğu kısıtlar yazısında anlatılıyor. Go 1.27 sürüm notlarına göre ayrıca fonksiyon tip çıkarımı, generic bir fonksiyonun eşleşen bir fonksiyon tipindeki değişkene atandığı ya da o tipe dönüştürüldüğü bütün bağlamlara genişletildi.

Kısıt, gövdede ne yapabileceğinizi belirler

Generic fonksiyonun gövdesinde bir tip parametresi üzerinde yalnızca kısıttaki bütün tiplerin desteklediği işlemleri yapabilirsiniz. any kısıtı her tipe izin verdiği için toplama, karşılaştırma gibi işlemlere izin vermez:

go
package main

import "fmt"

func Topla[T any](a, b T) T {
	return a + b
}

func main() { fmt.Println(Topla(1, 2)) }
text
# command-line-arguments
./main.go:6:9: invalid operation: operator + not defined on a (variable of type T constrained by any)

Toplama gerekiyorsa kısıtı sayı tiplerini içeren bir arayüzle daraltırsınız; sıralama gerekiyorsa cmp.Ordered kullanırsınız. Eşitlik karşılaştırması için comparable yeterlidir. Bu kısıtların nasıl yazıldığı kısıtlar yazısının konusudur.

FAQ'taki tuzak: alıcıda somut tip adı yazmak

Generic bir tipin belirli bir tip argümanı için özel metot yazmak isteyip func (k Kutu[string]) Ekle(s string) string yazmak sık görülen bir hatadır. FAQ'ın açıkladığı gibi derleyici buradaki string sözcüğünü metot içindeki tip parametresinin adı olarak alır; yani önceden tanımlı string tipini bu metot için gölgelemiş olursunuz. Sonuç kafa karıştırıcı bir hatadır:

go
package main

import "fmt"

type Kutu[T any] struct{ v T }

func (k Kutu[string]) Ekle(s string) string {
	return k.v + s
}

func main() { fmt.Println(Kutu[string]{"a"}.Ekle("b")) }
text
# command-line-arguments
./main.go:8:9: invalid operation: operator + not defined on k.v (variable of type string /* with string declared at ./main.go:7:14 */ constrained by any)

FAQ'ın açıklamasına göre alıcıdaki köşeli parantez içine yazılan adlar her zaman yeni tip parametresi adı olarak yorumlanır. Önceden tanımlı bir adı bu şekilde gölgelemek geçerli ama tuhaf bir şeydir ve FAQ'a göre çoğu zaman bir hatadır; alıcıda tip parametresi için T gibi bağımsız bir ad kullanın.

Generic metotlar (Go 1.27)

Go 1.27'ye kadar bir metot yalnızca alıcı tipinin tip parametrelerini kullanabiliyordu. Go 1.27 sürüm notlarına göre artık bir metot bildirimi kendi tip parametrelerini tanımlayabilir. Ancak arayüz metotları tip parametresi bildiremez ve arayüz metotları generic metotlarla uygulanamaz. Spesifikasyondaki List.Apply örneğini uyarladık:

go
package main

import (
	"fmt"
	"strconv"
)

type Liste[E any] []E

// Go 1.27: metot kendi tip parametresini (F) bildirebilir.
func (l Liste[E]) Donustur[F any](f func(E) F) Liste[F] {
	r := make(Liste[F], len(l))
	for i, x := range l {
		r[i] = f(x)
	}
	return r
}

func main() {
	l := Liste[int]{1, 2, 3}
	s := l.Donustur(strconv.Itoa)
	fmt.Printf("%q %T\n", s, s)
}

// Çıktı:
// ["1" "2" "3"] main.Liste[string]

Ne zaman generics kullanmalı, ne zaman kullanmamalı?

Ne zaman generics kullanmalı, ne zaman kullanmamalı? Dilim, map, kanal üzerinde eleman tipinden bağımsız işlem, Bağlı liste, ağaç gibi genel amaçlı veri yapısı, Bütün tipler için...

"When To Use Generics" yazısı genel kuralı tek cümleyle verir: aynı kodu, kopyalar arasındaki tek fark kullanılan tipler olacak şekilde birden çok kez yazdığınızı fark ettiğinizde tip parametresi kullanmayı düşünün. Yazının diğer önerileri şunlar:

DurumÖneri
Dilim, map, kanal üzerinde eleman tipinden bağımsız işlemTip parametresi uygun
Bağlı liste, ağaç gibi genel amaçlı veri yapısıTip parametresi uygun
Bütün tipler için aynı görünen metot uygulamasıTip parametresi uygun
Değerle yapılan tek şey bir metot çağırmakArayüz kullanın (ör. io.Reader)
Her tip için farklı metot uygulamasıArayüz ve ayrı uygulamalar
Metodu olmayan tiplerde, tipe göre farklı işlemYansıtma (reflect), ör. encoding/json

Yazı ayrıca performans konusunda uyarır: Go'nun generics uygulaması birçok durumda tip parametresi değerlerine arayüz değerlerine benzer biçimde davranır, bu yüzden bir arayüzü sırf hız için tip parametresine çevirmek genellikle kodu hızlandırmaz. FAQ'a göre standart derleyici, aynı "şekle" (boyut, işaretçi yerleşimi gibi özellikler) sahip tip argümanları için ortak tek bir örnekleme üretir. Bir başka öneri de karşılaştırma gibi bir işlem gerektiğinde, tip argümanından bir metot istemek yerine bir fonksiyon parametresi almaktır; böylece int gibi basit tipler de kolayca kullanılabilir.

Diğer dillerdeki generics ile farklar

FAQ, Go generics'ini diğer dillerle karşılaştırırken birkaç somut farkı not eder. C++ şablonları geleneksel olarak tip argümanlarına kısıt koymaz (C++20 kavramları isteğe bağlı kısıt getirir); Go'da ise her tip parametresinin bir kısıtı olması zorunludur ve bu kısıtlar, izin verilen tip argümanlarının kümesini tanımlayan arayüz tipleridir. C++ şablon üst programlamayı (template metaprogramming) destekler, Go desteklemez. Java'daki joker tipler (wildcard) ve bunlarla sağlanan kovaryans ile kontravaryans kavramları Go'da yoktur; FAQ bunun Go'daki generic tipleri çok daha basit kıldığını söyler. Dillerin genel karşılaştırması için programlama dilleri rehberine bakabilirsiniz.

Sık yapılan hatalar

  • Kısıtla başlamak: Blog yazısının ilk önerisi, programı tip tanımlayarak değil kod yazarak geliştirmektir. Önce somut fonksiyonu yazın, tekrar ortaya çıkınca tip parametresi ekleyin.
  • any ile işlem yapmaya çalışmak: any kısıtlı bir T üzerinde +, < veya == kullanılamaz; uygun kısıtı seçin.
  • Yalnızca dönüş değerinde geçen tip parametresi: Çıkarılamaz; çağrıda tip argümanını açıkça yazın.
  • Generic tipi örneklemeden kullanmak: Yigin değil Yigin[int] yazmalısınız.
  • Alıcıda somut tip adı yazmak: Kutu[string] alıcısı yeni bir string tip parametresi tanımlar.
  • Arayüzü gereksiz yere tip parametresine çevirmek: func Oku[T io.Reader](r T) yerine func Oku(r io.Reader) daha okunaklıdır ve blog yazısına göre büyük olasılıkla aynı hızdadır.

Sık Sorulan Sorular

Go generics hangi sürümle geldi?

Go 1.18 ile. Go Blog'a göre generics, Go'nun ilk açık kaynak sürümünden bu yana dilde yapılan en büyük değişikliktir. any takma adı da aynı sürümde eklendi.

Neden köşeli parantez kullanılıyor?

FAQ'a göre Java ve C++'taki açılı parantezler Go'da sözdizimi belirsizliği yaratırdı: a, b = w < x, y > (z) gibi bir satırın tip bilgisi olmadan karşılaştırma mı yoksa generic çağrı mı olduğu anlaşılamaz. Go'nun tip bilgisi olmadan ayrıştırılabilmesi temel bir tasarım kararı olduğu için köşeli parantez seçildi.

Generic kod çalışma zamanında yavaş mı?

Genel bir hız iddiasında bulunmak doğru olmaz; FAQ, derleyicinin her örnekleme için ayrı kod üretmek ile benzer örneklemeleri tek uygulamada birleştirmek arasında seçim yapabildiğini ve gelecekte bu dengenin değişebileceğini söyler. Ölçmek için benchmark yazısındaki yöntemleri kullanın.

Java'daki tip silme (type erasure) Go'da da var mı?

Hayır. FAQ'a göre Java derleme anında denetlenen generic tiplerin bilgisini çalışma zamanında siler; Go'da ise generic bir tipin yansıtma bilgisi tam derleme anı tip bilgisini içerir. Yukarıdaki %T çıktısında main.Yigin[string] görmenizin nedeni budur.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: Go öğrenme rehberi · Go generics kısıtları · Go interface · slices ve maps paketleri · Go 1.27 yenilikleri · Go iterator