115. Iterator & range-over-func (Go 1.23)
Menulis iterator kustom yang bisa dipakai langsung oleh perulangan range, memakai tipe iter.Seq dan iter.Seq2.
Masalah Sebelum Go 1.23
Sebelum Go 1.23, tidak ada cara seragam untuk mengiterasi struktur data buatan sendiri. Setiap pustaka mengarang polanya masing-masing: ada yang mengembalikan slice (boros memori), ada yang memakai channel (lambat dan rawan bocor goroutine), ada yang memakai objek Next() bergaya Java.
// 1. Kembalikan slice -- seluruh data harus muat di memori
func (t *Tree) Semua() []int { ... }
// 2. Channel -- lambat, dan bocor bila konsumen berhenti di tengah
func (t *Tree) Iterasi() <-chan int { ... }
// 3. Objek iterator -- verbose
it := t.Iterator()
for it.Next() { gunakan(it.Value()) }
range-over-func
Go 1.23 memperkenalkan kemampuan range untuk mengiterasi fungsi. Paket iter mendefinisikan dua tipe bakunya:
package iter type Seq[V any] func(yield func(V) bool) type Seq2[K, V any] func(yield func(K, V) bool)
Kuncinya ada pada nilai balik bool dari yield. Bila konsumen menghentikan perulangan (lewat break, return, atau panic), yield mengembalikan false dan iterator wajib berhenti. Inilah yang membuat pembersihan sumber daya menjadi benar secara otomatis.
import "iter"
type Pohon struct {
Nilai int
Kiri, Kanan *Pohon
}
// Penelusuran in-order sebagai iterator
func (p *Pohon) Semua() iter.Seq[int] {
return func(yield func(int) bool) {
p.telusuri(yield)
}
}
func (p *Pohon) telusuri(yield func(int) bool) bool {
if p == nil {
return true
}
return p.Kiri.telusuri(yield) &&
yield(p.Nilai) &&
p.Kanan.telusuri(yield)
}
// Dipakai persis seperti mengiterasi slice:
for nilai := range pohon.Semua() {
fmt.Println(nilai)
if nilai > 100 {
break // iterator berhenti dengan rapi
}
}
Karena iterator adalah fungsi biasa, defer bekerja seperti biasa. Ini menyelesaikan masalah kebocoran yang dulu menghantui iterator berbasis channel.
func BarisBerkas(nama string) iter.Seq2[string, error] {
return func(yield func(string, error) bool) {
f, err := os.Open(nama)
if err != nil {
yield("", err)
return
}
defer f.Close() // SELALU dipanggil, termasuk saat break
pemindai := bufio.NewScanner(f)
for pemindai.Scan() {
if !yield(pemindai.Text(), nil) {
return // konsumen berhenti -- defer tetap jalan
}
}
if err := pemindai.Err(); err != nil {
yield("", err)
}
}
}
for baris, err := range BarisBerkas("data.txt") {
if err != nil {
log.Fatal(err)
}
fmt.Println(baris)
}
Fungsi Pembantu di Paket iter
// Mengubah iterator "push" menjadi gaya "pull"
next, stop := iter.Pull(pohon.Semua())
defer stop()
for {
nilai, ada := next()
if !ada {
break
}
fmt.Println(nilai)
}
// Berguna untuk menggabungkan dua iterator secara berselang-seling,
// yang mustahil dilakukan dengan gaya push biasa.
Iterator yang mengabaikan hasil yield akan terus berjalan meski konsumen sudah break. Pada sumber data tak terbatas, ini berarti perulangan tak berujung. Selalu tulis if !yield(v) { return }.
Contoh program
package main
import "fmt"
// Simulasi iter.Seq: fungsi yang menerima yield
type Seq func(yield func(int) bool)
func Angka(dari, sampai int) Seq {
return func(yield func(int) bool) {
for i := dari; i <= sampai; i++ {
if !yield(i) {
fmt.Println(" (iterator berhenti lebih awal di", i, ")")
return
}
}
}
}
func main() {
fmt.Println("=== Iterasi penuh ===")
Angka(1, 5)(func(n int) bool {
fmt.Println(" nilai:", n)
return true
})
fmt.Println("\n=== Berhenti saat n > 3 (setara break) ===")
Angka(1, 10)(func(n int) bool {
if n > 3 {
return false
}
fmt.Println(" nilai:", n)
return true
})
}
116. Paket slices, maps & cmp
Menggantikan kode boilerplate manual dengan fungsi generik baku untuk mengurutkan, mencari, membandingkan, dan memanipulasi slice serta map.
Akhir dari Kode Berulang
Sebelum Go 1.21, setiap proyek punya paket utils berisi fungsi yang sama: cari indeks dalam slice, cek keberadaan elemen, ambil semua kunci map. Paket slices dan maps kini menyediakannya secara baku dan generik.
Paket slices
import "slices"
angka := []int{5, 2, 8, 1, 9}
slices.Sort(angka) // [1 2 5 8 9]
// Urut berdasarkan aturan sendiri
orang := []Orang{{"Budi", 30}, {"Ani", 25}}
slices.SortFunc(orang, func(a, b Orang) int {
return cmp.Compare(a.Umur, b.Umur)
})
// Versi stabil: elemen yang sama mempertahankan urutan aslinya
slices.SortStableFunc(orang, func(a, b Orang) int {
return strings.Compare(a.Nama, b.Nama)
})
slices.IsSorted(angka) // true
slices.Contains(angka, 8) // true
slices.Index(angka, 8) // 3, atau -1 bila tidak ada
slices.IndexFunc(orang, func(o Orang) bool {
return o.Umur > 28
})
// Pencarian biner -- HANYA pada slice yang sudah terurut
idx, ketemu := slices.BinarySearch(angka, 5)
a := []int{1, 2, 3}
b := []int{4, 5}
gabung := slices.Concat(a, b) // [1 2 3 4 5]
salinan := slices.Clone(a) // salinan dangkal
slices.Reverse(a) // [3 2 1]
// Buang elemen berurutan yang duplikat (perlu diurutkan dulu)
dedup := slices.Compact([]int{1, 1, 2, 2, 3}) // [1 2 3]
// Hapus indeks 1 sampai sebelum 3
sisa := slices.Delete([]int{1,2,3,4,5}, 1, 3) // [1 4 5]
// Sisipkan di indeks 2
hasil := slices.Insert([]int{1,2,5}, 2, 3, 4) // [1 2 3 4 5]
slices.Equal(a, b) // perbandingan elemen per elemen
slices.Max(angka)
slices.Min(angka)
Iterator di slices (Go 1.23+)
// Mengiterasi dengan indeks sebagai iter.Seq2
for i, v := range slices.All(angka) { ... }
// Hanya nilainya
for v := range slices.Values(angka) { ... }
// Dari belakang
for i, v := range slices.Backward(angka) { ... }
// Mengumpulkan iterator kembali menjadi slice
hasil := slices.Collect(pohon.Semua())
terurut := slices.Sorted(pohon.Semua())
Paket maps
import "maps"
m := map[string]int{"a": 1, "b": 2, "c": 3}
// Mengembalikan ITERATOR, bukan slice
for k := range maps.Keys(m) { ... }
for v := range maps.Values(m) { ... }
// Ubah menjadi slice terurut -- pola yang sangat sering dipakai
kunci := slices.Sorted(maps.Keys(m)) // [a b c]
salinan := maps.Clone(m)
maps.Copy(tujuan, sumber)
maps.Equal(m1, m2)
// Hapus berdasarkan kondisi
maps.DeleteFunc(m, func(k string, v int) bool {
return v < 2
})
Go dengan sengaja mengacak urutan iterasi map agar programmer tidak diam-diam bergantung padanya. Bila Anda butuh keluaran yang dapat direproduksi โ misalnya untuk log, JSON, atau uji โ selalu urutkan kuncinya dulu dengan slices.Sorted(maps.Keys(m)).
Paket cmp
import "cmp"
cmp.Compare(3, 5) // -1
cmp.Less("a", "b") // true
// Mengembalikan nilai bukan-kosong yang pertama
nama := cmp.Or(dariEnv, dariConfig, "bawaan")
// Pengurutan berjenjang: berdasarkan umur, lalu nama
slices.SortFunc(orang, func(a, b Orang) int {
return cmp.Or(
cmp.Compare(a.Umur, b.Umur),
cmp.Compare(a.Nama, b.Nama),
)
})
Sejak Go 1.21 tersedia juga tiga fungsi bawaan tanpa perlu import: min(a, b, c), max(a, b), serta clear(m) untuk mengosongkan map atau menolkan seluruh elemen slice.
Contoh program
package main
import (
"fmt"
"sort"
)
type Orang struct {
Nama string
Umur int
}
func main() {
angka := []int{5, 2, 8, 1, 9, 2}
fmt.Println("Asli :", angka)
sort.Ints(angka)
fmt.Println("Terurut :", angka)
// Simulasi slices.Compact
unik := angka[:1]
for _, v := range angka[1:] {
if v != unik[len(unik)-1] {
unik = append(unik, v)
}
}
fmt.Println("Tanpa dup :", unik)
// min & max bawaan Go 1.21+
fmt.Println("Min :", min(5, 2, 8))
fmt.Println("Max :", max(5, 2, 8))
// Map: kunci harus diurutkan agar keluaran konsisten
m := map[string]int{"c": 3, "a": 1, "b": 2}
kunci := make([]string, 0, len(m))
for k := range m {
kunci = append(kunci, k)
}
sort.Strings(kunci)
fmt.Println("Kunci urut:", kunci)
}
117. errgroup & Concurrency Terstruktur
Menjalankan banyak goroutine secara paralel dengan penanganan error, pembatalan otomatis, dan pembatasan jumlah pekerja memakai golang.org/x/sync/errgroup.
Kelemahan sync.WaitGroup
sync.WaitGroup hanya bisa menunggu. Ia tidak tahu apa-apa tentang error, dan tidak bisa membatalkan goroutine lain ketika salah satu gagal.
var wg sync.WaitGroup
var mu sync.Mutex
var errPertama error
for _, url := range daftarURL {
wg.Add(1)
go func(u string) {
defer wg.Done()
if err := ambil(u); err != nil {
mu.Lock()
if errPertama == nil {
errPertama = err
}
mu.Unlock()
}
}(url)
}
wg.Wait()
// Semua goroutine tetap berjalan sampai selesai
// meski yang pertama sudah gagal sejak awal.
errgroup
import "golang.org/x/sync/errgroup"
func ambilSemua(ctx context.Context, daftarURL []string) error {
g, ctx := errgroup.WithContext(ctx)
for _, url := range daftarURL {
g.Go(func() error {
return ambil(ctx, url) // cukup kembalikan error
})
}
return g.Wait() // error PERTAMA yang bukan nil
}
Begitu ada satu goroutine yang mengembalikan error, context yang dikembalikan akan dibatalkan. Semua goroutine lain yang menghormati context tersebut akan berhenti dengan sendirinya. Anda tidak perlu lagi mengatur mutex, channel penanda, atau pelacakan error secara manual.
Membatasi Jumlah Pekerja
g, ctx := errgroup.WithContext(ctx)
g.SetLimit(10) // maksimal 10 goroutine berjalan bersamaan
for _, berkas := range sejutaBerkas {
g.Go(func() error {
return proses(ctx, berkas)
})
}
return g.Wait()
Goroutine memang murah (sekitar 2 KB), tetapi sejuta goroutine berarti beberapa gigabyte memori, ditambah kemungkinan membuka sejuta koneksi sekaligus ke database atau API tujuan. SetLimit mengubah errgroup menjadi kolam pekerja dengan satu baris kode.
Mengumpulkan Hasil, Bukan Hanya Error
func ambilSemuaHasil(ctx context.Context, urls []string) ([]Hasil, error) {
hasil := make([]Hasil, len(urls)) // dialokasikan di muka
g, ctx := errgroup.WithContext(ctx)
g.SetLimit(runtime.NumCPU())
for i, url := range urls {
g.Go(func() error {
r, err := ambil(ctx, url)
if err != nil {
return fmt.Errorf("ambil %s: %w", url, err)
}
hasil[i] = r // AMAN: tiap goroutine menulis indeks berbeda
return nil
})
}
if err := g.Wait(); err != nil {
return nil, err
}
return hasil, nil
}
Menulis ke indeks yang berbeda dari slice yang sama tidak menimbulkan data race, karena tidak ada dua goroutine yang menyentuh lokasi memori yang sama. Yang tidak aman adalah append ke slice yang sama dari banyak goroutine, karena append mengubah panjang dan bisa memicu realokasi.
Bug Lama: Variabel Perulangan
// Di Go 1.21 dan sebelumnya, kode ini BUG:
for _, url := range urls {
go func() {
ambil(url) // semua goroutine melihat url yang SAMA
}()
}
// Solusi lama: url := url, atau kirim sebagai argumen.
// Sejak Go 1.22, variabel perulangan dibuat BARU di tiap iterasi,
// sehingga kode di atas kini berperilaku benar.
go di go.modSemantik baru hanya aktif bila modul Anda menyatakan go 1.22 atau lebih tinggi di go.mod. Modul lama tetap memakai perilaku lama demi kompatibilitas. Saat memutakhirkan modul lama, periksa kembali seluruh goroutine di dalam perulangan.
Menyebarkan Pembatalan dengan Benar
func proses(ctx context.Context, item Item) error {
// Selalu periksa pembatalan pada pekerjaan yang panjang
select {
case <-ctx.Done():
return ctx.Err()
default:
}
// Teruskan ctx ke SEMUA panggilan yang memblokir
req, _ := http.NewRequestWithContext(ctx, "GET", item.URL, nil)
resp, err := http.DefaultClient.Do(req)
...
row := db.QueryRowContext(ctx, "SELECT ...")
...
}
Pembatalan di Go bersifat kooperatif. Membatalkan context tidak menghentikan goroutine secara paksa โ ia hanya menutup sebuah channel. Bila kode Anda tidak pernah memeriksa ctx.Done() dan tidak meneruskan context ke pemanggilan yang memblokir, goroutine akan terus berjalan sampai selesai, dan errgroup akan menunggunya.
Contoh program
package main
import (
"errors"
"fmt"
"sync"
"time"
)
// Simulasi errgroup sederhana
type Grup struct {
wg sync.WaitGroup
mu sync.Mutex
err error
batal chan struct{}
sekali sync.Once
}
func (g *Grup) Go(fn func() error) {
g.wg.Add(1)
go func() {
defer g.wg.Done()
if err := fn(); err != nil {
g.mu.Lock()
if g.err == nil {
g.err = err
g.sekali.Do(func() { close(g.batal) })
}
g.mu.Unlock()
}
}()
}
func (g *Grup) Wait() error { g.wg.Wait(); return g.err }
func main() {
g := &Grup{batal: make(chan struct{})}
tugas := []string{"api-a", "api-b", "api-c"}
for i, nama := range tugas {
gagal := i == 1
g.Go(func() error {
time.Sleep(time.Duration(i+1) * 50 * time.Millisecond)
if gagal {
fmt.Println(" โ", nama, "GAGAL")
return errors.New(nama + ": koneksi ditolak")
}
fmt.Println(" โ", nama, "sukses")
return nil
})
}
if err := g.Wait(); err != nil {
fmt.Println("\nError pertama:", err)
return
}
fmt.Println("\nSemua berhasil")
}
118. Fuzzing & testing/synctest
Menemukan bug yang tidak terpikirkan lewat fuzzing bawaan, dan menguji kode konkuren berbasis waktu secara deterministik dengan testing/synctest.
Fuzzing: Membiarkan Mesin Mencari Kasus Uji
Uji biasa hanya memeriksa masukan yang Anda pikirkan. Fuzzing membangkitkan masukan secara otomatis dan cerdas, dipandu oleh cakupan kode, untuk menemukan yang tidak terpikirkan.
func FuzzUraiWaktu(f *testing.F) {
// Benih: contoh masukan yang valid sebagai titik awal
f.Add("12:30")
f.Add("00:00")
f.Add("23:59")
f.Fuzz(func(t *testing.T, s string) {
jam, menit, err := UraiWaktu(s)
if err != nil {
return // menolak masukan buruk itu WAJAR
}
// Bila berhasil diurai, hasilnya HARUS masuk akal
if jam < 0 || jam > 23 {
t.Errorf("jam di luar rentang: %d dari %q", jam, s)
}
if menit < 0 || menit > 59 {
t.Errorf("menit di luar rentang: %d dari %q", menit, s)
}
})
}
# Jalankan sebagai uji biasa (hanya memakai benih) go test ./... # Jalankan fuzzing sungguhan selama 30 detik go test -fuzz=FuzzUraiWaktu -fuzztime=30s # Masukan yang membuat gagal otomatis DISIMPAN ke # testdata/fuzz/FuzzUraiWaktu/ dan menjadi kasus uji permanen.
- Bolak-balik:
Urai(Format(x)) == xuntuk setiap x. - Tidak pernah panic: masukan apa pun boleh ditolak, tetapi tidak boleh membuat program crash.
- Invarian: hasil selalu berada dalam rentang yang sah.
- Kesetaraan: implementasi baru yang dioptimalkan memberi hasil sama dengan implementasi lama yang sederhana.
Penguraian JSON, YAML, protokol jaringan, format berkas, dan validasi masukan pengguna adalah tempat fuzzing paling sering menemukan bug nyata โ terutama panic akibat indeks di luar batas dan pointer nil pada masukan yang cacat.
testing/synctest: Menguji Waktu Tanpa Menunggu
Menguji kode yang memakai time.Sleep, time.After, atau timeout selalu bermasalah: uji menjadi lambat karena benar-benar menunggu, dan menjadi rapuh karena mesin CI yang sibuk membuat waktunya meleset.
func TestCacheKedaluwarsa(t *testing.T) {
c := NewCache(100 * time.Millisecond)
c.Set("a", 1)
time.Sleep(150 * time.Millisecond) // uji jadi lambat
if _, ada := c.Get("a"); ada {
t.Error("seharusnya sudah kedaluwarsa")
}
}
// Punya 50 uji seperti ini? Pipeline Anda bertambah lambat
// berdetik-detik, dan tetap gagal acak di runner yang sibuk.
import "testing/synctest"
func TestCacheKedaluwarsa(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
c := NewCache(100 * time.Millisecond)
c.Set("a", 1)
// Waktu di dalam gelembung ini PALSU.
// Maju 150 ms secara seketika.
time.Sleep(150 * time.Millisecond)
if _, ada := c.Get("a"); ada {
t.Error("seharusnya sudah kedaluwarsa")
}
})
}
Semua goroutine yang dibuat di dalam gelembung memakai jam virtual bersama. Ketika seluruh goroutine dalam gelembung sedang memblokir, jam virtual otomatis melompat maju ke timer terjadwal berikutnya. Akibatnya time.Sleep(1 * time.Hour) selesai seketika, dan hasilnya sepenuhnya deterministik โ tidak ada lagi uji yang gagal acak karena mesin sedang sibuk.
synctest.Test(t, func(t *testing.T) {
hasil := make(chan int, 1)
go func() {
time.Sleep(1 * time.Second)
hasil <- 42
}()
// Tunggu sampai semua goroutine dalam gelembung memblokir
synctest.Wait()
// Sekarang aman untuk memeriksa keadaan
select {
case v := <-hasil:
if v != 42 { t.Errorf("dapat %d", v) }
default:
t.Error("goroutine belum selesai")
}
})
Operasi yang benar-benar menyentuh dunia luar โ I/O jaringan, pembacaan berkas, panggilan sistem โ tidak ikut dipalsukan dan bisa membuat gelembung tidak pernah mencapai keadaan diam. synctest ditujukan untuk menguji logika penjadwalan dan waktu: timeout, retry dengan backoff, kedaluwarsa cache, rate limiter, dan debounce. Untuk pengujian yang menyentuh dependensi nyata, tetap gunakan uji integrasi biasa.
Perkakas Uji Lain yang Sering Terlupakan
# Deteksi data race -- WAJIB dijalankan di CI
go test -race ./...
# Laporan cakupan
go test -coverprofile=cover.out ./...
go tool cover -html=cover.out
# Benchmark beserta alokasi memori
go test -bench=. -benchmem
# Uji contoh yang keluarannya diverifikasi
func ExampleUraiWaktu() {
j, m, _ := UraiWaktu("12:30")
fmt.Println(j, m)
// Output: 12 30
}
# Subuji berbasis tabel dengan t.Parallel()
for _, kasus := range kasusUji {
t.Run(kasus.nama, func(t *testing.T) {
t.Parallel()
...
})
}
-race tidak boleh dilewatkanData race di Go tidak selalu menampakkan diri saat pengembangan; ia muncul di produksi di bawah beban tinggi, dan menghasilkan kerusakan data yang sangat sulit dilacak. Detektor race memperlambat uji beberapa kali lipat, tetapi menemukan bug yang praktis mustahil ditemukan dengan cara lain. Jadikan go test -race wajib di pipeline CI.
Contoh program
package main
import (
"fmt"
"strconv"
"strings"
)
// Fungsi yang akan di-fuzz
func UraiWaktu(s string) (jam, menit int, err error) {
bagian := strings.Split(s, ":")
if len(bagian) != 2 {
return 0, 0, fmt.Errorf("format tidak valid: %q", s)
}
jam, err = strconv.Atoi(bagian[0])
if err != nil {
return 0, 0, err
}
menit, err = strconv.Atoi(bagian[1])
if err != nil {
return 0, 0, err
}
if jam < 0 || jam > 23 || menit < 0 || menit > 59 {
return 0, 0, fmt.Errorf("di luar rentang: %q", s)
}
return jam, menit, nil
}
func main() {
// Masukan yang biasanya ditemukan fuzzer
masukan := []string{"12:30", "00:00", "23:59", "24:00", "-1:30", "12:60", "12", "::", "", "12:3a"}
for _, s := range masukan {
j, m, err := UraiWaktu(s)
if err != nil {
fmt.Printf("%-8q -> ditolak: %v\n", s, err)
continue
}
fmt.Printf("%-8q -> %02d:%02d โ\n", s, j, m)
}
}