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.Scanner adalah iterator — scanner.Scan() setara HasNext(), scanner.Text() setara Next(). database/sql.Rows juga iterator — rows.Next() dan rows.Scan(). filepath.Walk menggunakan 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() dan Next() 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, dan Reduce membuat client code lebih ekspresif tanpa perlu loop manual.
  • Standard library sudah pakai Iterator: bufio.Scanner, database/sql.Rows, dan filepath.Walk semua mengimplementasikan konsep yang sama.
  • Untuk slice sederhana, for-range sudah cukup — Iterator baru dibutuhkan ketika ada kerumitan traversal atau kebutuhan multiple traversal bersamaan.

← Sebelumnya: Memento   Berikutnya: Visitor →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact