Go'da iterator, bir dizinin öğelerini sırayla bir geri çağırma fonksiyonuna (geleneksel adıyla yield) veren sıradan bir fonksiyondur. Go 1.23 ile for range döngüsü, func(yield func(V) bool) biçimindeki fonksiyonları doğrudan dolaşabilir hâle geldi; yeni iter paketi de bu imzalara iter.Seq[V] ve iter.Seq2[K, V] adlarını verdi. Böylece kendi koleksiyon tipleriniz, ağaçlarınız ya da bir akıştan gelen satırlar, dilim ve map'ler kadar rahat dolaşılabiliyor.

Bu yazıda iterator imzalarının nasıl çalıştığını, yield fonksiyonunun false döndürmesinin ne anlama geldiğini, break ile erken çıkışta temizliğin nasıl yapıldığını, filtreleme gibi adaptörlerin nasıl yazıldığını ve iki diziyi aynı anda dolaşmak için iter.Pull kullanımını göreceksiniz. Örneklerin hepsi Go 1.27.3 ile çalıştırıldı. for döngüsünün diğer biçimleri için for döngüsü ve range yazısına, tip parametrelerinin temeli için generics rehberine bakabilirsiniz.

range ile dolaşılabilen fonksiyon imzaları

range ile dolaşılabilen fonksiyon imzaları: func(yield func() bool), func(yield func(V) bool), func(yield func(K, V) bool)

Dil spesifikasyonu, range ifadesi olarak her fonksiyonu kabul etmez. Fonksiyon tek bir parametre almalı, bu parametre de sıfır, bir ya da iki argüman alıp bool döndüren bir fonksiyon olmalıdır. Go'da "iterator" denince bu üç tipten biri kastedilir. Spesifikasyon ayrıca yield fonksiyonunun değişken sayıda argüman alamayacağını (variadic olamayacağını) belirtir.

İmzaDöngü değişkenleriStandart ad
func(yield func() bool)yok (for range f)adı yok
func(yield func(V) bool)viter.Seq[V]
func(yield func(K, V) bool)k, viter.Seq2[K, V]

Döngü çalışırken derleyici, döngü gövdesini temsil eden bir yield fonksiyonu üretir ve iterator'ü bu fonksiyonla çağırır. Iterator yield(v) çağırdığında v döngü değişkenine atanır ve gövde bir kez çalışır. Gövde normal biçimde biterse yield true döndürür ve iterator bir sonraki değeri verebilir. Gövde döngüyü sonlandırırsa (örneğin break ile) yield false döndürür; spesifikasyona göre bu noktadan sonra yield bir daha çağrılmamalıdır. Iterator fonksiyonu döndüğünde döngü de biter.

İlk iterator: geri sayım

Aşağıdaki GeriSay fonksiyonu bir iter.Seq[int] döndürür. İkinci döngüde break kullanıldığında yield false döner; iterator bunu fark edip kendi temizlik işini yapar ve return ile çıkar. Gerçek bir programda bu nokta bir dosyayı kapatmak, bir kilidi bırakmak ya da bir sorguyu iptal etmek olabilir.

go
package main

import (
	"fmt"
	"iter"
)

// GeriSay, n'den 1'e kadar sayan bir iterator döndürür.
func GeriSay(n int) iter.Seq[int] {
	return func(yield func(int) bool) {
		for i := n; i >= 1; i-- {
			if !yield(i) {
				fmt.Println("  (döngü erken bitti, temizlik yapılıyor)")
				return
			}
		}
	}
}

func main() {
	for v := range GeriSay(3) {
		fmt.Println(v)
	}

	for v := range GeriSay(10) {
		if v == 8 {
			break
		}
		fmt.Println("ikinci:", v)
	}
}

// Çıktı:
// 3
// 2
// 1
// ikinci: 10
// ikinci: 9
//   (döngü erken bitti, temizlik yapılıyor)

Çıktıdaki sıra önemlidir: temizlik mesajı, 9 yazıldıktan sonra ve 8 için gövde break ile çıktığında görünür. Yani döngü kontrolü iterator'e geri döner ve iterator, ancak yield sonucunu kontrol ettiği için erken çıkıştan haberdar olur. if !yield(v) { return } kalıbı bu yüzden her iterator'ün omurgasıdır.

iter.Seq, iter.Seq2 ve adlandırma kuralları

iter paketi yalnızca iki tip tanımı ve iki yardımcı fonksiyon içerir. Seq[V] tek değer, Seq2[K, V] ise çift değer üreten iterator'dür; çiftler genellikle anahtar ve değer ya da indeks ve değerdir. Paket belgesi, iterator kabul eden ya da döndüren fonksiyonların bu standart tipleri kullanmasını ister, çünkü range döngüleri ve diğer adaptörlerle uyum ancak böyle sağlanır.

Adlandırma için de bir gelenek vardır: koleksiyonun tüm öğelerini veren metot All adını alır. Bir tip birden fazla dizi sunuyorsa ad hangi dizinin verildiğini söyler (belgedeki örnekte bir ülke tipi için Cities ve Languages). Birden fazla dolaşma sırası varsa sıra ada yansır: All baştan sona, Backward sondan başa, ağaçlar için Preorder gibi. Ek ayar gereken iterator'ler, örneğin bir aralıktaki anahtarları veren bir Scan(min, max), parametre alan bir kurucu fonksiyonla sunulur.

Belgede ayrıca tek kullanımlık (single-use) iterator kavramı geçer. Çoğu iterator her çağrıldığında diziyi baştan dolaşır. Ancak geri sarılamayan bir veri akışından okuyan iterator'ler bunu yapamaz: erken durdurulduktan sonra yeniden çağrılırsa akışa kaldığı yerden devam edebilir, dizi bittikten sonra çağrılırsa hiç değer üretmez. Bu tür bir iterator döndüren fonksiyonun belge yorumunda bu durumun açıkça yazılması beklenir.

Kendi tipinize All metodu eklemek

Aşağıdaki örnekte map tabanlı generic bir küme tipi tanımlanıyor ve All metodu bir iter.Seq[E] döndürüyor. Map dolaşma sırası belirsiz olduğu için sonucu slices.Sorted ile sıralı bir dilime topluyoruz. Aynı örnekte standart kütüphanenin hazır iterator fonksiyonları maps.Keys ve slices.Backward da kullanılıyor.

go
package main

import (
	"fmt"
	"iter"
	"maps"
	"slices"
)

type Kume[E comparable] struct {
	m map[E]struct{}
}

func YeniKume[E comparable](ogeler ...E) *Kume[E] {
	k := &Kume[E]{m: make(map[E]struct{})}
	for _, o := range ogeler {
		k.m[o] = struct{}{}
	}
	return k
}

// All, kümedeki tüm öğeler üzerinde bir iterator döndürür.
func (k *Kume[E]) All() iter.Seq[E] {
	return func(yield func(E) bool) {
		for o := range k.m {
			if !yield(o) {
				return
			}
		}
	}
}

func main() {
	k := YeniKume("elma", "armut", "kiraz")
	fmt.Println(slices.Sorted(k.All()))

	stok := map[string]int{"kalem": 12, "defter": 5, "silgi": 30}
	for _, ad := range slices.Sorted(maps.Keys(stok)) {
		fmt.Println(ad, stok[ad])
	}

	renkler := []string{"kırmızı", "yeşil", "mavi"}
	for i, r := range slices.Backward(renkler) {
		fmt.Println(i, r)
	}
}

// Çıktı:
// [armut elma kiraz]
// defter 5
// kalem 12
// silgi 30
// 2 mavi
// 1 yeşil
// 0 kırmızı

slices.Sorted(maps.Keys(m)) kalıbı, map anahtarlarını sıralı dolaşmanın en kısa yoludur ve iter paket belgesinde de örnek olarak verilir. Burada maps.Keys bir dilim değil iterator döndürür; anahtarlar ancak slices.Sorted onları topladığında bellekte bir dilime yazılır. Map kullanımının ayrıntıları için map kullanımı, bu fonksiyonların tam listesi için slices, maps ve sort paketleri yazısına bakabilirsiniz.

Standart kütüphanedeki iterator fonksiyonları

Standart kütüphanedeki iterator fonksiyonları: slices.All(s), slices.Values(s), slices.Backward(s), slices.Collect(seq)

Go 1.23 sürüm notları slices ve maps paketlerine iterator ile çalışan bir grup fonksiyon eklendiğini duyurur; Go 1.24 sürüm notları ise strings ve bytes paketlerine satır ve parça iterator'leri ekler. En sık kullanılanlar şunlardır:

FonksiyonDöndürdüğüNe yaparSürüm
slices.All(s)iter.Seq2[int, E]indeks ve değer çiftleri1.23
slices.Values(s)iter.Seq[E]yalnızca değerler1.23
slices.Backward(s)iter.Seq2[int, E]sondan başa indeks ve değer1.23
slices.Collect(seq)[]Eiterator'ü yeni dilime toplar1.23
slices.AppendSeq(s, seq)[]Evar olan dilime ekler1.23
slices.Sorted(seq)[]Etoplar ve sıralar1.23
slices.Chunk(s, n)iter.Seq[[]E]n öğelik alt dilimler1.23
maps.All / Keys / ValuesSeq2 / Seq / Seqmap içeriği (sıra belirsiz)1.23
maps.Collect(seq2)map[K]Vçiftleri yeni map'e toplar1.23
maps.Insert(m, seq2)yokçiftleri var olan map'e ekler1.23
strings.Lines(s)iter.Seq[string]satır sonu dahil satırlar1.24
strings.SplitSeq(s, sep)iter.Seq[string]Split'in dilim ayırmayan hâli1.24
strings.FieldsSeq(s)iter.Seq[string]boşluklara göre parçalar1.24

slices.Collect ve slices.Sorted, iterator boşsa nil döndürür; slices.AppendSeq ise dizi boşsa verilen dilimin nil olma durumunu korur. slices.Chunk son parça dışında tüm parçaları tam n öğe yapar, n 1'den küçükse panic olur. Aynı fonksiyonların bytes sürümleri (bytes.Lines, bytes.SplitSeq ve diğerleri) bayt dilimleri üzerinde aynı işi görür.

Adaptör yazmak: filtreleme ve dönüştürme

Standart bir iterator tanımının en büyük kazancı, her diziyle çalışan küçük adaptörler yazabilmektir. Go Blog yazısındaki Filter örneği bir iterator alır ve yalnızca koşulu sağlayan öğeleri veren yeni bir iterator döndürür. Aşağıda buna benzer bir Filtre ve her öğeye bir fonksiyon uygulayan Esle yazıp, strings.SplitSeq ile birlikte zincirliyoruz. Blog yazısı, bu tür bir Filter fonksiyonunun standart kütüphanede bulunmadığını not eder.

go
package main

import (
	"fmt"
	"iter"
	"slices"
	"strings"
)

// Filtre, f'nin true döndürdüğü öğelerden oluşan yeni bir dizi verir.
func Filtre[V any](f func(V) bool, s iter.Seq[V]) iter.Seq[V] {
	return func(yield func(V) bool) {
		for v := range s {
			if f(v) {
				if !yield(v) {
					return
				}
			}
		}
	}
}

// Esle, her öğeye f'yi uygular.
func Esle[V, W any](f func(V) W, s iter.Seq[V]) iter.Seq[W] {
	return func(yield func(W) bool) {
		for v := range s {
			if !yield(f(v)) {
				return
			}
		}
	}
}

func main() {
	kayit := "ankara,izmir,,bursa,antalya"
	dolu := func(s string) bool { return s != "" }
	sehirler := slices.Collect(Esle(strings.ToUpper, Filtre(dolu, strings.SplitSeq(kayit, ","))))
	fmt.Println(sehirler, len(sehirler))

	metin := "birinci satır\nikinci satır\nüçüncü"
	for satir := range strings.Lines(metin) {
		fmt.Printf("%q\n", satir)
	}
}

// Çıktı:
// [ANKARA IZMIR BURSA ANTALYA] 4
// "birinci satır\n"
// "ikinci satır\n"
// "üçüncü"

Zincirdeki hiçbir aşama ara bir dilim oluşturmaz: SplitSeq bir parça üretir, Filtre onu kontrol eder, Esle büyük harfe çevirir ve slices.Collect sonucu dilime ekler. Ardından gelen değer ancak bu tur bittikten sonra üretilir. strings.Lines çıktısında satır sonu karakterinin parçaya dahil olduğuna, son satırda ise olmadığına dikkat edin.

Go Blog yazısı burada dengeli bir uyarı da yapar: bu işi düz bir döngüyle yazmak çoğu durumda daha açıktır ve Go ekibi herkesin her şeyi iterator zinciriyle yazmasını teşvik etmez. Adaptörlerin değeri, aynı fonksiyonun girdi map de olsa, dilim de olsa, dosya satırı da olsa değişmeden çalışmasıdır.

Özyinelemeli yapılar: ikili ağacı dolaşmak

Push iterator'lerin en rahat olduğu yer ağaç gibi özyinelemeli yapılardır. Konumu tutmak için ayrı bir yığın yönetmeniz gerekmez; goroutine'in çağrı yığını bu işi sizin için yapar. Blog yazısındaki tekniği uygulayalım: yardımcı metot bool döndürür ve yield false verdiği anda bu sonuç özyinelemenin en üstüne kadar taşınır.

go
package main

import (
	"fmt"
	"iter"
)

// Agac, basit bir ikili arama ağacıdır.
type Agac[E any] struct {
	deger    E
	sol, sag *Agac[E]
}

// All, ağaçtaki değerleri sıralı (in-order) dolaşır.
func (t *Agac[E]) All() iter.Seq[E] {
	return func(yield func(E) bool) {
		t.it(yield)
	}
}

func (t *Agac[E]) it(yield func(E) bool) bool {
	if t == nil {
		return true
	}
	return t.sol.it(yield) && yield(t.deger) && t.sag.it(yield)
}

func main() {
	kok := &Agac[int]{deger: 50,
		sol: &Agac[int]{deger: 30, sol: &Agac[int]{deger: 10}},
		sag: &Agac[int]{deger: 70, sag: &Agac[int]{deger: 90}},
	}
	var gorulen []int
	for v := range kok.All() {
		gorulen = append(gorulen, v)
		if v == 70 {
			break
		}
	}
	fmt.Println(gorulen)
}

// Çıktı:
// [10 30 50 70]

Döngü 70'te break ile çıkınca 90 hiç ziyaret edilmez. && operatörü kısa devre yaptığı için yield false döndükten sonra sağ alt ağaca inilmez ve spesifikasyonun "bir daha çağırma" kuralı kendiliğinden sağlanır. All metodunun iç fonksiyonu ise bool sonucu yok sayar, çünkü range ile kullanılan fonksiyon tipleri bir şey döndürmez.

Pull iterator: iter.Pull ile değer çekmek

Pull iterator: iter.Pull ile değer çekmek: Değeri kim taşır, range ile kullanım, Erken çıkışta temizlik, Tipik kullanım

Standart iterator'ler "push" tarzındadır: değerleri yield fonksiyonuna iterator iter. Bazen bunun tersi gerekir, örneğin iki diziyi aynı anda adım adım ilerletmek. iter.Pull bir push iterator'ü alır ve iki fonksiyon döndürür: her çağrıldığında sıradaki değeri ve geçerli olup olmadığını veren next, iterasyonu sonlandıran stop.

go
package main

import (
	"fmt"
	"iter"
	"slices"
)

// Esit, iki dizinin aynı öğeleri aynı sırada içerip içermediğini bildirir.
func Esit[E comparable](s1, s2 iter.Seq[E]) bool {
	next1, stop1 := iter.Pull(s1)
	defer stop1()
	next2, stop2 := iter.Pull(s2)
	defer stop2()
	for {
		v1, ok1 := next1()
		v2, ok2 := next2()
		if !ok1 {
			return !ok2
		}
		if ok1 != ok2 || v1 != v2 {
			return false
		}
	}
}

func main() {
	a := slices.Values([]int{1, 2, 3})
	b := slices.Values([]int{1, 2, 3})
	c := slices.Values([]int{1, 2})
	fmt.Println(Esit(a, b), Esit(a, c))

	next, stop := iter.Pull(slices.Values([]string{"x", "y"}))
	defer stop()
	for range 3 {
		v, ok := next()
		fmt.Printf("%q %v\n", v, ok)
	}
}

// Çıktı:
// true false
// "x" true
// "y" true
// "" false

Paket belgesine göre dizi bittiğinde next sıfır değeri ve false döndürür; dizi bittikten ya da stop çağrıldıktan sonra next'i çağırmaya devam etmek geçerlidir ve aynı sonucu verir. stop birden fazla kez çağrılabilir. Uymanız gereken kural şudur: diziyi sonuna kadar tüketmeyecekseniz stop'u mutlaka çağırın; bu, push iterator'ün temizlik koduna ulaşmasını sağlar. Bu yüzden defer stop() yazmak alışkanlık olmalıdır. next ve stop'u birden fazla goroutine'den aynı anda çağırmak hatadır; iterator next sırasında panic olursa aynı değerle next de panic olur.

ÖzellikPush (iter.Seq)Pull (iter.Pull)
Değeri kim taşıriterator yield'i çağırırsiz next() çağırırsınız
range ile kullanımdoğrudandoğrudan değil, düz for ile
Erken çıkışta temizlikbreak otomatik sağlarstop() çağırmanız gerekir
Tipik kullanımtek diziyi dolaşmakiki diziyi birlikte ilerletmek, eşleştirmek

Sık yapılan hatalar

yield false döndükten sonra devam etmek

En sık hata, yield sonucunu kontrol etmemektir. Aşağıdaki iterator dönüş değerini yok sayıyor; döngü gövdesi break ile çıktığı hâlde bir sonraki değeri vermeye çalışınca çalışma zamanı programı durduruyor:

go
package main

import "fmt"

// Hatali, yield false döndürse bile durmuyor.
func Hatali(yield func(int) bool) {
	for i := range 5 {
		yield(i)
	}
}

func main() {
	for v := range Hatali {
		fmt.Println(v)
		if v == 1 {
			break
		}
	}
}

// Çıktı:
// 0
// 1
// panic: runtime error: range function continued iteration after function for loop body returned false

Kural basit: yield çağrısını her zaman if !yield(v) { return } içinde yapın. İç içe döngülerde ya da özyinelemede bu sonucu en üste kadar taşıyın.

Pull kullanırken stop çağırmamak

iter.Pull ile alınan iterator'ü yarıda bırakıp stop çağırmazsanız push iterator hiçbir zaman temizlik koduna ulaşamaz. Go Blog yazısı, push iterator'ün goroutine başlatmış ya da temizlenmesi gereken yapılar kurmuş olabileceğini hatırlatır. Diziyi sonuna kadar okumuş olsanız bile defer stop() yazmak en sade çözümdür.

Map tabanlı iterator'lerde sıra beklemek

maps.Keys, maps.Values, maps.All ve map üzerinde dolaşan kendi iterator'leriniz belirli bir sıra vermez; maps belgesi sıranın çağrıdan çağrıya aynı olacağının garanti edilmediğini açıkça yazar. Sıra gerekiyorsa sonucu slices.Sorted ile toplayın.

go.mod'daki dil sürümünü unutmak

range over func, Go 1.23 dil özelliğidir. Modülünüzün go.mod dosyası daha eski bir go satırı içeriyorsa derleyici bu söz dizimini kabul etmez. Blog yazısı dört yol sayar: go get go@1.23 (ya da yalnızca satırı değiştirmek için go mod edit -go=1.23), go.mod'u elle düzenlemek ya da yalnızca belirli bir dosyada kullanmak için //go:build go1.23 yapı etiketi eklemek. Modül dosyası için go mod rehberine bakın.

Iterator üzerinden diziyi değiştirmeye çalışmak

Iterator yalnızca değerleri verir; diziyi değiştirmenin doğrudan bir yolunu sunmaz. Paket belgesi, değiştirme gerekiyorsa ek işlemler sunan bir konum (position) tipi tanımlayıp bu konumlar üzerinde iterator vermeyi önerir; örnekteki ağaç tipi her konum için Value, Set ve Delete metotları sunar ve bu konum yalnızca kendisine verildiği yield çağrısı süresince geçerlidir.

Sık Sorulan Sorular

iter.Seq bir arayüz mü, fonksiyon tipi mi?

Fonksiyon tipidir: type Seq[V any] func(yield func(V) bool). Bu yüzden herhangi bir fonksiyon literali, imzası uyduğu sürece iter.Seq olarak kullanılabilir. Resmî örneklerde slices.Collect'e doğrudan bir fonksiyon literali verildiğini görebilirsiniz.

Iterator'ler dilim döndürmekten daha mı hızlı?

Genel bir hız iddiası belgelerde yoktur. Blog yazısının söylediği şudur: bytes.Split gibi fonksiyonlar tüm parçaları tutan bir dilim ayırır; satırları iterator ile vermek bu dilimi ayırmaz ve çöp toplayıcıya daha az iş bırakır. Gerçek etkiyi kendi kodunuzda ölçmek için benchmark yazısındaki yöntemleri kullanın.

Iterator'ü range olmadan kullanabilir miyim?

Evet. Iterator sıradan bir fonksiyondur; kendi yazdığınız bir yield fonksiyonuyla doğrudan çağırabilirsiniz, örneğin s.All()(func(v E) bool { fmt.Println(v); return true }). Blog yazısı bunun özel bir avantajı olmadığını, yalnızca yield'in sihirli olmadığını göstermek için verildiğini belirtir.

Kanal kullanmak yerine neden iterator?

Bir goroutine ve kanal ile de değer üretebilirsiniz, ama o zaman goroutine yaşam döngüsünü ve kanalın kapatılmasını yönetmeniz gerekir. Push iterator aynı goroutine içinde, sıradan fonksiyon çağrılarıyla çalışır ve break ile çıkışta temizliği yield sonucuyla öğrenir. Kanallar için channel rehberine bakabilirsiniz.

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 · slices, maps ve sort paketleri · for döngüsü ve range · Go generics · map kullanımı · Go nedir?