Iter Package

Apa Itu pkg/iter?

Singkatnya, pkg/iter adalah paket bantu yang memungkinkan Anda membuat iterator kustom dengan sangat mudah.

Di Go 1.22+, loop for...range mendapat kemampuan baru: ia bisa melakukan looping pada sebuah fungsi. Paket iter adalah alat standar untuk membuat fungsi-fungsi tersebut.

Ini memungkinkan Anda menulis kode yang lebih bersih dan efisien (terutama dalam hal memori) untuk logika perulangan yang kompleks.

Paket ini memberi kita dua "cetakan" iterator utama:

  1. iter.Seq[V]: Untuk iterator yang menghasilkan satu nilai (misalnya string atau int).

  2. iter.Seq2[K, V]: Untuk iterator yang menghasilkan dua nilai, yaitu key dan value (mirip seperti map).

Konsep Inti: Fungsi Ajaib yield

Kekuatan utama dari pkg/iter ada pada satu konsep: fungsi yield (artinya "menghasilkan" atau "memberikan").

Pikirkan yield sebagai cara iterator Anda untuk "mengirim" satu nilai kembali ke loop for...range.

  1. Iterator kustom Anda akan menerima fungsi yield sebagai parameter.

  2. Setiap kali Anda memanggil yield(value) di dalam iterator, nilai tersebut "dikirim" dan diterima oleh variabel loop Anda.

  3. yield juga mengembalikan bool. Ini adalah sinyal dari loop di luar.

    • Jika yield mengembalikan true, itu artinya loop masih berjalan dan Anda bisa lanjut.

    • Jika yield mengembalikan false, itu artinya loop di luar bilang "Stop!" (misalnya karena break atau return). Iterator Anda wajib berhenti saat itu juga.


Membuat Iterator Kustom

Mari kita lihat tiga contoh, dari yang paling sederhana hingga yang menggunakan key-value.

Contoh 1: iter.Seq[V] - Generator Angka

Mari kita buat iterator Count(n) yang menghasilkan angka dari 1 sampai n.

package main

import (
	"fmt"
	"iter"
)

// Count adalah generator yang menghasilkan angka dari 1 sampai n.
// Dia mengembalikan iter.Seq[int], yang aslinya adalah:
// func(yield func(int) bool)
func Count(n int) iter.Seq[int] {
	// 1. Kita kembalikan sebuah fungsi
	return func(yield func(int) bool) {
		// 2. Ini adalah logika iterator kita
		for i := 1; i <= n; i++ {
			// 3. Kirim nilai 'i' ke loop dan cek sinyal 'stop'
			if !yield(i) {
				return // Berhenti jika loop di luar meminta (misal: break)
			}
		}
	}
}

func main() {
	// Gunakan langsung di for...range!
	fmt.Println("Menghitung sampai 4:")
	for v := range Count(4) {
		fmt.Println(v)
	}

	fmt.Println("\nMenghitung sampai 10, tapi berhenti di 2:")
	for v := range Count(10) {
		fmt.Println(v)
		if v == 2 {
			break // yield(i) di dalam Count() akan mengembalikan false
		}
	}
}

/* Output:
Menghitung sampai 4:
1
2
3
4

Menghitung sampai 10, tapi berhenti di 2:
1
2
*/

Contoh 2: iter.Seq[V] - Adapter Slice Terbalik

Anda bisa membuat "adapter" untuk tipe data yang sudah ada. Mari kita buat iterator untuk slice tapi secara terbalik.

package main

import (
	"fmt"
	"iter"
)

// Reverse adalah "adapter" yang membuat slice bisa di-loop terbalik.
func Reverse[E any](s []E) iter.Seq[E] {
	return func(yield func(E) bool) {
		for i := len(s) - 1; i >= 0; i-- {
			if !yield(s[i]) {
				return // Patuhi sinyal break
			}
		}
	}
}

func main() {
	names := []string{"Budi", "Cici", "Dedi"}

	fmt.Println("Nama terbalik:")
	for name := range Reverse(names) {
		fmt.Println(name)
	}
}

/* Output:
Nama terbalik:
Dedi
Cici
Budi
*/

Contoh 3: iter.Seq2[K, V] - Iterator Key-Value

Sekarang, mari kita buat iterator yang menghasilkan field dan value dari sebuah struct. Kita akan pakai iter.Seq2.

package main

import (
	"fmt"
	"iter"
)

type User struct {
	ID   int
	Name string
	Role string
}

// Fields mengembalikan iterator Key-Value untuk struct User.
// Perhatikan, kita pakai iter.Seq2[string, any]
func (u User) Fields() iter.Seq2[string, any] {
	return func(yield func(string, any) bool) {
		// 1. yield sekarang menerima DUA argumen (key, value)
		// 2. Kita tetap wajib cek return bool-nya.
		if !yield("ID", u.ID) {
			return
		}
		if !yield("Name", u.Name) {
			return
		}
		if !yield("Role", u.Role) {
			return
		}
	}
}

func main() {
	user := User{ID: 101, Name: "Eka", Role: "Admin"}

	// Kita bisa menangkap dua variabel di loop!
	for key, value := range user.Fields() {
		fmt.Printf("%s: %v\n", key, value)
	}
}

/* Output:
ID: 101
Name: Eka
Role: Admin
*/

Best Practice dan Aturan Penggunaan

Karena fitur ini sudah stabil, berikut adalah aturan kapan harus (dan tidak harus) menggunakannya.

1. Kapan Harus Digunakan?

Gunakan iter saat Anda membutuhkan Evaluasi Malas (Lazy Evaluation).

Artinya, Anda hanya memproses data tepat saat dibutuhkan, satu per satu, tanpa memuat semuanya ke memori sekaligus.

  • Generator: Menghasilkan urutan angka (misal: Fibonacci) tanpa batas, tanpa harus menyimpannya di slice raksasa.

  • Data Streaming: Membaca file 10GB baris per baris. Anda tidak mau memuat 10GB itu ke RAM. Iterator akan yield satu baris, memori bebas, lalu yield baris berikutnya.

  • Menyembunyikan Kompleksitas: Anda punya struktur data rumit (seperti Tree atau Graph). Anda bisa buat metode .Nodes() yang mengembalikan iter.Seq[Node] untuk menyembunyikan logika traversal yang rumit di dalamnya.

2. Kapan TIDAK Boleh Digunakan? (Performa)

Jangan over-engineering. Jika Anda hanya perlu looping slice atau map biasa, selalu gunakan for...range standar.

// BURUK: (Lebih lambat dan berlebihan)
// for v := range Reverse(Reverse(mySlice)) { ... }

// BAIK: (Jauh lebih cepat dan idiomatis)
// for _, v := range mySlice { ... }

Alasan: Setiap panggilan yield adalah sebuah function call, yang memiliki overhead performa kecil. Loop for...range standar di slice atau map dioptimalkan habis-habisan oleh compiler Go dan akan selalu lebih cepat.

3. WAJIB: Selalu Patuhi Sinyal yield

Ini adalah aturan paling penting. Selalu cek nilai bool yang dikembalikan yield.

// WAJIB DILAKUKAN
if !yield(value) {
    return // Segera hentikan iterator Anda
}

Jika Anda mengabaikannya, loop Anda akan terus berjalan bahkan setelah pemanggil melakukan break, yang bisa menyebabkan bug atau kebocoran sumber daya.

4. Error Handling

Ini adalah bagian yang sedikit tricky: yield tidak bisa mengembalikan error.

Jadi, bagaimana jika iterator Anda gagal di tengah jalan (misal, koneksi jaringan putus)?

Pola Terbaik: Pisahkan error "setup" dari error "iterasi".

Fungsi Anda harus mengembalikan (iter.Seq[T], error). Pemanggil wajib mengecek error sebelum memulai loop.

// Pola yang disarankan untuk iterator yang bisa gagal
func ReadLines(path string) (iter.Seq[string], func() error, error) {
	file, err := os.Open(path) // 1. Error 'Setup'
	if err != nil {
		return nil, nil, err
	}
	// (defer file.Close() harus ditangani dengan hati-hati)

	scanner := bufio.NewScanner(file)

	// 2. Iteratornya sendiri 'bersih'
	seq := func(yield func(string) bool) {
		for scanner.Scan() {
			if !yield(scanner.Text()) {
				return // Berhenti jika diminta
			}
		}
	}

	// 3. Kembalikan fungsi untuk cek error 'saat iterasi'
	checkErr := func() error {
		if err := scanner.Err(); err != nil {
			return err
		}
		return file.Close() // Tutup dan cek error penutupan
	}

	return seq, checkErr, nil
}

// --- Cara Penggunaannya ---
func main() {
	lines, checkErr, err := ReadLines("file.txt")
	if err != nil {
		log.Fatal(err) // Gagal saat setup (misal: file tidak ada)
	}

	for line := range lines {
		fmt.Println(line)
	}

	// Cek error yang mungkin terjadi saat iterasi (misal: I/O error)
	if err := checkErr(); err != nil {
		log.Fatal(err)
	}
}

Kesimpulan

  • Gunakan pkg/iter saat Anda butuh evaluasi malas (lazy), streaming data, atau generator untuk menghemat memori.

  • Jangan gunakan pkg/iter jika for...range biasa di slice atau map sudah cukup.

  • Selalu cek if !yield(...) { return }.

Last updated