Iterator Pattern #
Sebuah ProductCatalog menyimpan produk dalam struktur tree — kategori punya sub-kategori, sub-kategori punya produk. Kode yang perlu memproses semua produk tidak seharusnya tahu apakah data disimpan sebagai slice flat, tree, linked list, atau hasil query database yang di-stream. Iterator Pattern memisahkan dua hal yang sering dicampur: cara menyimpan data dan cara menelusuri data. Koleksi menyediakan Iterator; client menggunakan Iterator; tidak ada yang perlu tahu detail internal penyimpanan. Jika suatu saat struktur penyimpanan berubah dari slice ke database, client tidak perlu diubah — hanya implementasi Iterator-nya.
Apa itu Iterator Pattern? #
Iterator Pattern adalah behavioral design pattern yang menyediakan cara untuk mengakses elemen-elemen dalam sebuah koleksi secara berurutan tanpa mengekspos representasi internalnya. Pattern ini memisahkan logika traversal (cara menelusuri) dari struktur data (cara menyimpan), sehingga keduanya bisa berkembang secara independen.
Tiga properti yang mendefinisikan Iterator Pattern:
- Enkapsulasi traversal — client tidak perlu tahu apakah koleksi adalah slice, tree, graph, atau hasil database query
- Multiple iterator — beberapa traversal bisa berjalan secara bersamaan pada koleksi yang sama tanpa saling mengganggu
- Lazy evaluation — elemen bisa di-generate saat diminta, bukan semuanya sekaligus; memungkinkan iterasi atas dataset yang sangat besar
flowchart LR
subgraph "Tanpa Iterator"
C1[Client] -->|"tahu struktur internal"| SL["slice: catalog.products[i]"]
C1 -->|"tahu cara traversal tree"| TR["tree: recursiveWalk(root)"]
C1 -->|"tahu cara query DB"| DB["db: rows.Next(), rows.Scan()"]
end
subgraph "Dengan Iterator"
C2[Client] -->|"it.HasNext() / it.Next()"| IT[Iterator\ninterface]
IT --> SL2[SliceIterator]
IT --> TR2[TreeIterator]
IT --> DB2[DBIterator]
end
Iterator di Golang: Lebih dari Satu Cara #
Golang memiliki beberapa cara idiomatik mengimplementasikan Iterator, masing-masing dengan trade-off yang berbeda.
Cara 1: Interface Klasik (HasNext/Next) #
// Iterator interface klasik — paling eksplisit, paling mirip OOP tradisional
type Iterator[T any] interface {
HasNext() bool
Next() T
Reset()
}
Cara 2: Channel-based (Golang idiomatik) #
// Iterator berbasis channel — natural untuk Golang, mendukung range
func Iterate(collection []Product) <-chan Product {
ch := make(chan Product)
go func() {
defer close(ch)
for _, p := range collection {
ch <- p
}
}()
return ch
}
// Penggunaan dengan range
for product := range Iterate(catalog.Products()) {
fmt.Println(product.Name)
}
Cara 3: Functional Iterator (Go 1.23+) #
// iter.Seq adalah type yang diperkenalkan di Go 1.23
// func(yield func(T) bool)
func (c *ProductCatalog) All() iter.Seq[Product] {
return func(yield func(Product) bool) {
for _, p := range c.products {
if !yield(p) {
return // yield mengembalikan false = stop iterasi
}
}
}
}
// Penggunaan dengan range (Go 1.23+)
for product := range catalog.All() {
fmt.Println(product.Name)
}
Cara 4: Callback-based #
// Iterator berbasis callback — sederhana untuk koleksi kecil
func (c *ProductCatalog) ForEach(fn func(product Product) bool) {
for _, p := range c.products {
if !fn(p) {
break // fn mengembalikan false = stop
}
}
}
// Penggunaan
catalog.ForEach(func(p Product) bool {
if p.Price > 1000000 {
return false // stop di produk pertama yang mahal
}
fmt.Println(p.Name)
return true
})
Perbandingan keempat pendekatan:
| Pendekatan | Kelebihan | Kekurangan | Cocok Untuk |
|---|---|---|---|
| Interface HasNext/Next | Familiar, eksplisit, mudah di-mock | Verbose, tidak bisa pakai range |
Library yang perlu dipahami oleh banyak developer |
| Channel-based | Natural Golang, pakai range |
Overhead goroutine, perlu context untuk cancel | Concurrent iteration, pipeline |
| Functional (iter.Seq) | Paling idiomatic Go 1.23+, pakai range |
Go 1.23+ saja | Modern codebase |
| Callback | Paling sederhana | Tidak support lazy stop dengan mudah | Koleksi kecil, internal use |
Implementasi Lengkap: Product Catalog dengan Multiple Iterator #
Koleksi dan Iterator Interface #
package catalog
// Product merepresentasikan satu produk dalam katalog.
type Product struct {
ID string
Name string
Category string
Price float64
InStock bool
Tags []string
}
// ProductIterator adalah interface untuk iterasi produk.
// Menggunakan pendekatan interface klasik agar kompatibel dengan semua versi Go.
type ProductIterator interface {
HasNext() bool
Next() *Product
Reset()
}
// ProductCollection adalah Aggregate interface — koleksi yang bisa membuat iterator.
type ProductCollection interface {
CreateIterator() ProductIterator
CreateFilteredIterator(predicate func(*Product) bool) ProductIterator
CreateSortedIterator(less func(a, b *Product) bool) ProductIterator
Count() int
}
Concrete Aggregate: ProductCatalog #
package catalog
import (
"sort"
)
// ProductCatalog adalah Concrete Aggregate — menyimpan produk dalam slice.
type ProductCatalog struct {
products []*Product
}
func NewProductCatalog() *ProductCatalog {
return &ProductCatalog{products: make([]*Product, 0)}
}
// Add menambahkan produk ke katalog.
func (c *ProductCatalog) Add(product *Product) {
c.products = append(c.products, product)
}
// Count mengembalikan jumlah produk dalam katalog.
func (c *ProductCatalog) Count() int { return len(c.products) }
// CreateIterator membuat iterator dasar yang menelusuri semua produk.
func (c *ProductCatalog) CreateIterator() ProductIterator {
return &SliceIterator{
products: c.products,
index: 0,
}
}
// CreateFilteredIterator membuat iterator yang hanya mengembalikan produk
// yang memenuhi predicate tertentu.
func (c *ProductCatalog) CreateFilteredIterator(predicate func(*Product) bool) ProductIterator {
return &FilteredIterator{
source: c.CreateIterator(),
predicate: predicate,
}
}
// CreateSortedIterator membuat iterator yang mengembalikan produk dalam urutan tertentu.
// Membuat salinan slice untuk menghindari modifikasi koleksi asli.
func (c *ProductCatalog) CreateSortedIterator(less func(a, b *Product) bool) ProductIterator {
sorted := make([]*Product, len(c.products))
copy(sorted, c.products)
sort.Slice(sorted, func(i, j int) bool {
return less(sorted[i], sorted[j])
})
return &SliceIterator{products: sorted, index: 0}
}
// InStockIterator shortcut: iterator hanya untuk produk yang tersedia.
func (c *ProductCatalog) InStockIterator() ProductIterator {
return c.CreateFilteredIterator(func(p *Product) bool {
return p.InStock
})
}
// CategoryIterator shortcut: iterator untuk kategori tertentu.
func (c *ProductCatalog) CategoryIterator(category string) ProductIterator {
return c.CreateFilteredIterator(func(p *Product) bool {
return p.Category == category
})
}
// PriceRangeIterator shortcut: iterator untuk rentang harga tertentu.
func (c *ProductCatalog) PriceRangeIterator(minPrice, maxPrice float64) ProductIterator {
return c.CreateFilteredIterator(func(p *Product) bool {
return p.Price >= minPrice && p.Price <= maxPrice
})
}
Concrete Iterators #
package catalog
// SliceIterator menelusuri produk dalam slice secara berurutan.
type SliceIterator struct {
products []*Product
index int
}
func (it *SliceIterator) HasNext() bool {
return it.index < len(it.products)
}
func (it *SliceIterator) Next() *Product {
if !it.HasNext() {
return nil
}
product := it.products[it.index]
it.index++
return product
}
func (it *SliceIterator) Reset() {
it.index = 0
}
// FilteredIterator membungkus iterator lain dan hanya mengembalikan
// elemen yang memenuhi predicate. Ini adalah contoh Decorator pada Iterator.
type FilteredIterator struct {
source ProductIterator
predicate func(*Product) bool
next *Product // elemen berikutnya yang sudah di-prefetch
fetched bool // apakah sudah di-prefetch
}
func (it *FilteredIterator) prefetch() {
for it.source.HasNext() {
product := it.source.Next()
if it.predicate(product) {
it.next = product
it.fetched = true
return
}
}
it.next = nil
it.fetched = true
}
func (it *FilteredIterator) HasNext() bool {
if !it.fetched {
it.prefetch()
}
return it.next != nil
}
func (it *FilteredIterator) Next() *Product {
if !it.fetched {
it.prefetch()
}
result := it.next
it.fetched = false
it.next = nil
return result
}
func (it *FilteredIterator) Reset() {
it.source.Reset()
it.next = nil
it.fetched = false
}
// ReverseIterator menelusuri slice dari belakang ke depan.
type ReverseIterator struct {
products []*Product
index int
}
func NewReverseIterator(products []*Product) ProductIterator {
return &ReverseIterator{
products: products,
index: len(products) - 1,
}
}
func (it *ReverseIterator) HasNext() bool {
return it.index >= 0
}
func (it *ReverseIterator) Next() *Product {
if !it.HasNext() {
return nil
}
product := it.products[it.index]
it.index--
return product
}
func (it *ReverseIterator) Reset() {
it.index = len(it.products) - 1
}
Penggunaan: Client Tidak Tahu Struktur Internal #
func main() {
catalog := catalog.NewProductCatalog()
catalog.Add(&catalog.Product{ID: "P001", Name: "Laptop Pro", Category: "Electronics",
Price: 15000000, InStock: true, Tags: []string{"laptop", "premium"}})
catalog.Add(&catalog.Product{ID: "P002", Name: "Wireless Mouse", Category: "Electronics",
Price: 350000, InStock: true, Tags: []string{"mouse", "wireless"}})
catalog.Add(&catalog.Product{ID: "P003", Name: "Standing Desk", Category: "Furniture",
Price: 5000000, InStock: false, Tags: []string{"desk", "ergonomic"}})
catalog.Add(&catalog.Product{ID: "P004", Name: "Mechanical Keyboard", Category: "Electronics",
Price: 2500000, InStock: true, Tags: []string{"keyboard", "mechanical"}})
// Iterator dasar: semua produk
fmt.Println("=== Semua Produk ===")
it := catalog.CreateIterator()
for it.HasNext() {
p := it.Next()
fmt.Printf(" %s: Rp %.0f\n", p.Name, p.Price)
}
// Iterator dengan filter: hanya yang tersedia
fmt.Println("\n=== Produk Tersedia ===")
it = catalog.InStockIterator()
for it.HasNext() {
p := it.Next()
fmt.Printf(" %s\n", p.Name)
}
// Iterator dengan sorting: urut harga termurah
fmt.Println("\n=== Urut Harga (Termurah) ===")
it = catalog.CreateSortedIterator(func(a, b *catalog.Product) bool {
return a.Price < b.Price
})
for it.HasNext() {
p := it.Next()
fmt.Printf(" %s: Rp %.0f\n", p.Name, p.Price)
}
// Multiple iterator bersamaan — tidak saling mengganggu
fmt.Println("\n=== Multiple Iterator Bersamaan ===")
it1 := catalog.CreateIterator()
it2 := catalog.CreateIterator()
_ = it1.Next() // it1 maju satu langkah
fmt.Printf("it1 berikutnya: %s\n", it1.Next().Name)
fmt.Printf("it2 berikutnya: %s\n", it2.Next().Name) // it2 masih dari awal
}
Tree Iterator: Traversal Rekursif #
Untuk struktur tree (seperti file system atau kategori produk bersarang), Iterator menyembunyikan kompleksitas traversal rekursif dari client.
package tree
// CategoryNode merepresentasikan node dalam tree kategori.
type CategoryNode struct {
Name string
Products []Product
Children []*CategoryNode
}
// DFSIterator melakukan depth-first search traversal pada tree kategori.
// Client tidak perlu tahu bahwa struktur ini adalah tree — cukup gunakan HasNext/Next.
type DFSIterator struct {
stack []*CategoryNode
products []Product // buffer produk dari node saat ini
prodIdx int
}
func NewDFSIterator(root *CategoryNode) *DFSIterator {
it := &DFSIterator{
stack: make([]*CategoryNode, 0),
}
it.stack = append(it.stack, root)
it.loadNextNode()
return it
}
func (it *DFSIterator) loadNextNode() {
it.products = nil
it.prodIdx = 0
for len(it.stack) > 0 && len(it.products) == 0 {
// Pop dari stack
node := it.stack[len(it.stack)-1]
it.stack = it.stack[:len(it.stack)-1]
// Push children ke stack (dalam urutan terbalik agar DFS benar)
for i := len(node.Children) - 1; i >= 0; i-- {
it.stack = append(it.stack, node.Children[i])
}
it.products = node.Products
}
}
func (it *DFSIterator) HasNext() bool {
return it.prodIdx < len(it.products)
}
func (it *DFSIterator) Next() Product {
if !it.HasNext() {
return Product{}
}
p := it.products[it.prodIdx]
it.prodIdx++
if it.prodIdx >= len(it.products) {
it.loadNextNode()
}
return p
}
func (it *DFSIterator) Reset() {
// Reset tidak mudah untuk tree iterator — buat iterator baru
panic("DFSIterator does not support Reset — create a new iterator")
}
// BFSIterator melakukan breadth-first search traversal.
type BFSIterator struct {
queue []*CategoryNode
products []Product
prodIdx int
}
func NewBFSIterator(root *CategoryNode) *BFSIterator {
it := &BFSIterator{
queue: []*CategoryNode{root},
}
it.loadNextNode()
return it
}
func (it *BFSIterator) loadNextNode() {
it.products = nil
it.prodIdx = 0
for len(it.queue) > 0 && len(it.products) == 0 {
node := it.queue[0]
it.queue = it.queue[1:]
it.queue = append(it.queue, node.Children...)
it.products = node.Products
}
}
func (it *BFSIterator) HasNext() bool { return it.prodIdx < len(it.products) }
func (it *BFSIterator) Next() Product {
if !it.HasNext() {
return Product{}
}
p := it.products[it.prodIdx]
it.prodIdx++
if it.prodIdx >= len(it.products) {
it.loadNextNode()
}
return p
}
func (it *BFSIterator) Reset() {
panic("BFSIterator does not support Reset — create a new iterator")
}
Database Cursor sebagai Iterator #
Iterator Pattern sangat natural untuk database cursor — setiap baris hanya di-load saat diminta, tidak sekaligus.
package dbiter
import (
"context"
"database/sql"
"fmt"
)
// UserIterator iterates over database rows lazily — satu baris per Next().
// Tidak semua baris di-load ke memori sekaligus.
type UserIterator struct {
rows *sql.Rows
current *User
err error
done bool
}
// User merepresentasikan satu baris dari tabel users.
type User struct {
ID int
Name string
Email string
Role string
}
// NewUserIterator membuat iterator dari query database.
func NewUserIterator(ctx context.Context, db *sql.DB, query string, args ...interface{}) (*UserIterator, error) {
rows, err := db.QueryContext(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("query failed: %w", err)
}
return &UserIterator{rows: rows}, nil
}
// HasNext memeriksa apakah masih ada baris berikutnya.
// Secara internal, ini melakukan prefetch baris berikutnya.
func (it *UserIterator) HasNext() bool {
if it.done {
return false
}
if !it.rows.Next() {
it.done = true
it.rows.Close()
return false
}
var u User
if err := it.rows.Scan(&u.ID, &u.Name, &u.Email, &u.Role); err != nil {
it.err = err
it.done = true
return false
}
it.current = &u
return true
}
// Next mengembalikan user saat ini (yang sudah di-prefetch oleh HasNext).
func (it *UserIterator) Next() *User {
return it.current
}
// Error mengembalikan error terakhir jika ada.
func (it *UserIterator) Error() error { return it.err }
// Close menutup database rows.
func (it *UserIterator) Close() error { return it.rows.Close() }
// Penggunaan
func processAllUsers(ctx context.Context, db *sql.DB) error {
it, err := NewUserIterator(ctx, db,
"SELECT id, name, email, role FROM users WHERE is_active = true ORDER BY id")
if err != nil {
return err
}
defer it.Close()
for it.HasNext() {
user := it.Next()
// Proses satu user pada satu waktu — tidak semua di memori
fmt.Printf("Processing: %s (%s)\n", user.Name, user.Email)
}
return it.Error()
}
Channel-based Iterator untuk Concurrent Processing #
Channel-based iterator sangat cocok ketika data dihasilkan secara asinkron atau memerlukan concurrent processing.
package pipeline
import (
"context"
"fmt"
)
// StreamProducts menghasilkan produk satu per satu melalui channel.
// Consumer bisa membaca produk tanpa tahu bagaimana mereka di-generate.
func StreamProducts(ctx context.Context, catalog *ProductCatalog) <-chan *Product {
ch := make(chan *Product, 10) // buffer untuk mengurangi blocking
go func() {
defer close(ch)
it := catalog.CreateIterator()
for it.HasNext() {
select {
case <-ctx.Done():
return // context dibatalkan — hentikan streaming
case ch <- it.Next():
// produk dikirim ke consumer
}
}
}()
return ch
}
// FilterStream membungkus channel stream dengan filter — Decorator pada channel iterator.
func FilterStream(ctx context.Context, in <-chan *Product, predicate func(*Product) bool) <-chan *Product {
out := make(chan *Product, 10)
go func() {
defer close(out)
for {
select {
case <-ctx.Done():
return
case p, ok := <-in:
if !ok {
return
}
if predicate(p) {
out <- p
}
}
}
}()
return out
}
// Penggunaan: pipeline dengan channel iterator
func buildPipeline(ctx context.Context, catalog *ProductCatalog) {
// Stream semua produk
all := StreamProducts(ctx, catalog)
// Filter hanya yang in-stock
inStock := FilterStream(ctx, all, func(p *Product) bool {
return p.InStock
})
// Filter hanya yang harganya di bawah 5 juta
affordable := FilterStream(ctx, inStock, func(p *Product) bool {
return p.Price < 5000000
})
// Consumer — proses hasil pipeline
for product := range affordable {
fmt.Printf("Affordable in-stock: %s (Rp %.0f)\n", product.Name, product.Price)
}
}
Iterator dengan Helper Functions #
Fungsi-fungsi helper yang bekerja dengan iterator generik membuat client code lebih ekspresif.
// Collect mengumpulkan semua elemen dari iterator ke dalam slice.
func Collect(it ProductIterator) []*Product {
var results []*Product
for it.HasNext() {
results = append(results, it.Next())
}
return results
}
// First mengembalikan elemen pertama yang memenuhi predicate.
func First(it ProductIterator, predicate func(*Product) bool) *Product {
for it.HasNext() {
p := it.Next()
if predicate(p) {
return p
}
}
return nil
}
// Count menghitung elemen yang memenuhi predicate.
func Count(it ProductIterator, predicate func(*Product) bool) int {
count := 0
for it.HasNext() {
if predicate(it.Next()) {
count++
}
}
return count
}
// Reduce mengagregasi semua elemen menjadi satu nilai.
func Reduce[T any](it ProductIterator, initial T, fn func(acc T, p *Product) T) T {
acc := initial
for it.HasNext() {
acc = fn(acc, it.Next())
}
return acc
}
// Penggunaan helper functions
func analyzeStock(catalog *ProductCatalog) {
it := catalog.CreateIterator()
totalValue := Reduce(it, 0.0, func(acc float64, p *Product) float64 {
if p.InStock {
return acc + p.Price
}
return acc
})
fmt.Printf("Total value in stock: Rp %.0f\n", totalValue)
it.Reset()
cheapest := First(catalog.InStockIterator(), func(p *Product) bool {
return p.Price < 500000
})
if cheapest != nil {
fmt.Printf("Cheapest under 500k: %s\n", cheapest.Name)
}
}
Testing Iterator Pattern #
func TestSliceIterator_TraversesAllElements(t *testing.T) {
catalog := NewProductCatalog()
catalog.Add(&Product{ID: "P1", Name: "Product 1", Price: 100})
catalog.Add(&Product{ID: "P2", Name: "Product 2", Price: 200})
catalog.Add(&Product{ID: "P3", Name: "Product 3", Price: 300})
it := catalog.CreateIterator()
var visited []string
for it.HasNext() {
p := it.Next()
visited = append(visited, p.ID)
}
if len(visited) != 3 {
t.Errorf("expected 3 elements, got %d", len(visited))
}
if visited[0] != "P1" || visited[1] != "P2" || visited[2] != "P3" {
t.Errorf("wrong traversal order: %v", visited)
}
}
func TestSliceIterator_Reset(t *testing.T) {
catalog := NewProductCatalog()
catalog.Add(&Product{ID: "P1", Name: "Product 1"})
catalog.Add(&Product{ID: "P2", Name: "Product 2"})
it := catalog.CreateIterator()
_ = it.Next() // advance satu langkah
_ = it.Next() // advance lagi
if it.HasNext() {
t.Error("expected no more elements")
}
it.Reset()
if !it.HasNext() {
t.Error("after reset, should have elements again")
}
if p := it.Next(); p.ID != "P1" {
t.Errorf("after reset, first element should be P1, got %s", p.ID)
}
}
func TestFilteredIterator_OnlyReturnsMatching(t *testing.T) {
catalog := NewProductCatalog()
catalog.Add(&Product{ID: "P1", Name: "Laptop", InStock: true, Price: 10000000})
catalog.Add(&Product{ID: "P2", Name: "Mouse", InStock: false, Price: 200000})
catalog.Add(&Product{ID: "P3", Name: "Keyboard", InStock: true, Price: 1500000})
it := catalog.InStockIterator()
var inStock []string
for it.HasNext() {
inStock = append(inStock, it.Next().ID)
}
if len(inStock) != 2 {
t.Errorf("expected 2 in-stock products, got %d", len(inStock))
}
if inStock[0] != "P1" || inStock[1] != "P3" {
t.Errorf("wrong filtered elements: %v", inStock)
}
}
func TestMultipleIterators_IndependentTraversal(t *testing.T) {
catalog := NewProductCatalog()
for i := 1; i <= 5; i++ {
catalog.Add(&Product{ID: fmt.Sprintf("P%d", i), Name: fmt.Sprintf("Product %d", i)})
}
it1 := catalog.CreateIterator()
it2 := catalog.CreateIterator()
// Maju it1 dua langkah
_ = it1.Next()
p1 := it1.Next()
// it2 masih dari awal — tidak terpengaruh oleh it1
p2 := it2.Next()
if p1.ID != "P2" {
t.Errorf("it1 second element: expected P2, got %s", p1.ID)
}
if p2.ID != "P1" {
t.Errorf("it2 first element: expected P1, got %s", p2.ID)
}
}
func TestSortedIterator_CorrectOrder(t *testing.T) {
catalog := NewProductCatalog()
catalog.Add(&Product{ID: "P3", Price: 300})
catalog.Add(&Product{ID: "P1", Price: 100})
catalog.Add(&Product{ID: "P2", Price: 200})
it := catalog.CreateSortedIterator(func(a, b *Product) bool {
return a.Price < b.Price
})
var order []string
for it.HasNext() {
order = append(order, it.Next().ID)
}
if order[0] != "P1" || order[1] != "P2" || order[2] != "P3" {
t.Errorf("wrong sort order: %v", order)
}
}
Kapan Menggunakan dan Kapan Tidak #
GUNAKAN Iterator jika:
✓ Perlu menelusuri koleksi dengan struktur internal yang kompleks (tree, graph)
✓ Perlu multiple traversal bersamaan pada koleksi yang sama
✓ Perlu lazy evaluation untuk dataset besar (database cursor, file stream)
✓ Ingin memisahkan logika traversal dari logika penyimpanan
✓ Perlu beberapa cara traversal berbeda (DFS, BFS, filtered, sorted)
HINDARI Iterator jika:
✗ Koleksi adalah slice/array sederhana — for-range sudah cukup dan lebih idiomatik
✗ Hanya perlu satu traversal, satu kali — for-range langsung lebih sederhana
✗ Koleksi sangat kecil — overhead interface tidak sepadan
✗ Golang 1.22+ tersedia dan koleksi sudah implement iter.Seq — gunakan itu
Iterator di Standard Library Golang
bufio.Scanneradalah iterator —scanner.Scan()setaraHasNext(),scanner.Text()setaraNext().database/sql.Rowsjuga iterator —rows.Next()danrows.Scan().filepath.Walkmenggunakan callback-based iteration. Memahami Iterator Pattern membantu kamu membaca dan memahami API-API standar ini dengan lebih intuitif.
Checklist Review Iterator #
DESAIN:
□ Iterator mengenkapsulasi traversal — client tidak tahu struktur internal koleksi
□ Setiap Iterator adalah objek independen — beberapa bisa berjalan bersamaan
□ Koleksi menyediakan factory method (CreateIterator) untuk membuat iterator
□ Iterator tidak memodifikasi koleksi saat traversal
IMPLEMENTASI:
□ HasNext() tidak punya side effect — memanggil berkali-kali aman
□ Next() hanya boleh dipanggil setelah HasNext() mengembalikan true
□ Reset() mengembalikan iterator ke posisi awal
□ FilteredIterator melakukan prefetch dengan benar untuk HasNext() yang akurat
LAZY EVALUATION:
□ Database iterator tidak load semua baris sekaligus
□ Channel iterator menggunakan context untuk cancellation
□ Iterator menutup resource (rows.Close(), channel) saat selesai atau dibatalkan
TESTING:
□ Semua elemen dikunjungi tepat sekali
□ Urutan traversal diverifikasi (untuk sorted/DFS/BFS iterator)
□ Multiple iterator berjalan independen satu sama lain
□ Reset() mengembalikan ke posisi awal dengan benar
□ FilteredIterator hanya mengembalikan elemen yang memenuhi predicate
Ringkasan #
- Iterator memisahkan “cara menelusuri” dari “cara menyimpan” — client menggunakan
HasNext()danNext()tanpa perlu tahu apakah koleksinya slice, tree, database cursor, atau generator.- Empat pendekatan di Golang: interface klasik (HasNext/Next), channel-based, functional (iter.Seq di Go 1.23+), dan callback — pilih sesuai konteks dan versi Go.
- Multiple iterator independen — setiap iterator memiliki state traversal sendiri; beberapa bisa berjalan bersamaan pada koleksi yang sama tanpa saling mengganggu.
- FilteredIterator adalah Decorator pada Iterator — membungkus iterator lain dan hanya meneruskan elemen yang memenuhi predicate; tidak perlu mengubah koleksi asli.
- Lazy evaluation untuk dataset besar — database cursor dan channel-based iterator hanya memuat satu elemen pada satu waktu; tidak perlu load seluruh dataset ke memori.
- Helper functions seperti
Collect,First,Count, danReducemembuat client code lebih ekspresif tanpa perlu loop manual.- Standard library sudah pakai Iterator:
bufio.Scanner,database/sql.Rows, danfilepath.Walksemua mengimplementasikan konsep yang sama.- Untuk slice sederhana, for-range sudah cukup — Iterator baru dibutuhkan ketika ada kerumitan traversal atau kebutuhan multiple traversal bersamaan.