Perulangan #
Go hanya punya satu keyword untuk perulangan: for. Tidak ada while. Tidak ada do-while. Tidak ada foreach. Keputusan ini bukan karena Go tidak mampu — ini disengaja untuk menjaga konsistensi. Ketika semua developer Go menggunakan satu keyword yang sama untuk semua pola perulangan, kode dari satu developer langsung mudah dibaca oleh developer lain. Dan ternyata, for yang satu itu cukup fleksibel untuk mengekspresikan semua pola perulangan yang pernah ada.
Tiga Bentuk for
#
for di Go bisa ditulis dalam tiga bentuk utama, masing-masing punya konteks penggunaan yang tepat.
Bentuk 1: Classic Three-Component #
Bentuk paling familiar bagi developer yang datang dari C, Java, atau JavaScript:
// for init; condition; post { }
for i := 0; i < 5; i++ {
fmt.Println(i)
}
// Output: 0 1 2 3 4
Tiga komponen dipisahkan titik koma:
- init (
i := 0) — dieksekusi sekali sebelum iterasi pertama; variabel yang dideklarasikan di sini hanya hidup dalam scope loop - condition (
i < 5) — diperiksa sebelum setiap iterasi; loop berhenti ketikafalse - post (
i++) — dieksekusi setelah setiap iterasi sebelum pengecekan condition berikutnya
Setiap komponen bersifat opsional — kamu bisa menghilangkan salah satu atau keduanya:
// Hanya condition — setara while
i := 0
for i < 5 {
fmt.Println(i)
i++
}
// Hanya init dan post — condition selalu true → infinite loop
for i := 0; ; i++ {
if i >= 5 {
break
}
fmt.Println(i)
}
// Tanpa semua komponen → infinite loop
for {
// loop selamanya
}
Bentuk 2: Condition Only (setara while)
#
Ketika hanya ada kondisi, for berperilaku seperti while di bahasa lain:
// Membaca input sampai kondisi terpenuhi
attempts := 0
for attempts < maxRetry {
err := tryConnect()
if err == nil {
break
}
attempts++
time.Sleep(time.Second * time.Duration(attempts))
}
// Proses data stream sampai habis
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
processLine(line)
}
Bentuk 3: Infinite Loop #
for tanpa apapun adalah infinite loop — bukan code smell, tapi pola yang sangat sah di Go untuk server, daemon, dan worker:
// Server loop — berjalan selamanya menunggu request
for {
conn, err := listener.Accept()
if err != nil {
log.Println("accept error:", err)
continue
}
go handleConnection(conn)
}
// Worker loop — memproses job dari channel
for {
select {
case job := <-jobQueue:
processJob(job)
case <-stopSignal:
return // keluar dari infinite loop
}
}
for-range — Iterasi Koleksi
#
range adalah cara idiomatik Go untuk mengiterasi koleksi. Ia mengembalikan dua nilai: index (atau key) dan value. Kamu bisa mengabaikan salah satunya dengan blank identifier _.
Range pada Slice dan Array #
buah := []string{"apel", "mangga", "jeruk", "durian"}
// Dua nilai: index dan value
for i, v := range buah {
fmt.Printf("[%d] %s\n", i, v)
}
// Hanya value — index diabaikan
for _, v := range buah {
fmt.Println(v)
}
// Hanya index — value diabaikan
for i := range buah {
fmt.Printf("index: %d\n", i)
}
// Modifikasi elemen via index (range memberi SALINAN value)
angka := []int{1, 2, 3, 4, 5}
for i := range angka {
angka[i] *= 2 // ✓ modifikasi lewat index, bukan lewat v
}
fmt.Println(angka) // [2 4 6 8 10]
rangememberikan salinan (copy) dari value, bukan reference. Memodifikasivdalam loop tidak mempengaruhi slice asli:angka := []int{1, 2, 3} for _, v := range angka { v *= 2 // ✗ memodifikasi salinan lokal, bukan elemen asli } fmt.Println(angka) // [1 2 3] — tidak berubah! // BENAR: modifikasi lewat index for i := range angka { angka[i] *= 2 // ✓ } fmt.Println(angka) // [2 4 6]
Range pada String — Per Rune, Bukan Per Byte #
Ini salah satu perilaku yang paling sering mengejutkan. range pada string mengiterasi per rune (karakter Unicode), bukan per byte. Index yang dikembalikan adalah posisi byte, bukan posisi karakter:
s := "Hello, 世界"
// range per RUNE
for i, r := range s {
fmt.Printf("byte index %d: %c (U+%04X)\n", i, r, r)
}
// Output:
// byte index 0: H (U+0048)
// byte index 1: e (U+0065)
// byte index 2: l (U+006C)
// byte index 3: l (U+006C)
// byte index 4: o (U+006F)
// byte index 5: , (U+002C)
// byte index 6: (U+0020)
// byte index 7: 世 (U+4E16) ← index 7, karakter selanjutnya di byte 10!
// byte index 10: 界 (U+754C) ← 世 memakan 3 bytes (7,8,9)
// Iterasi per BYTE (tidak otomatis, harus konversi ke []byte)
for i, b := range []byte(s) {
fmt.Printf("byte index %d: 0x%02X\n", i, b)
}
Range pada Map #
Range pada map mengembalikan key dan value. Urutan iterasi tidak dijamin — berbeda setiap kali program dijalankan:
harga := map[string]int{
"apel": 5000,
"mangga": 8000,
"jeruk": 4500,
}
// Urutan acak setiap run
for k, v := range harga {
fmt.Printf("%s: Rp%d\n", k, v)
}
// Jika butuh urutan konsisten, sort keynya dulu
import "sort"
keys := make([]string, 0, len(harga))
for k := range harga {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
fmt.Printf("%s: Rp%d\n", k, harga[k])
}
// Output selalu berurutan: apel:5000, jeruk:4500, mangga:8000
Range pada Channel #
Range pada channel membaca nilai sampai channel ditutup dengan close():
ch := make(chan int)
// Producer — mengirim nilai dan menutup channel
go func() {
for i := 0; i < 5; i++ {
ch <- i
}
close(ch) // PENTING: tutup channel agar range berhenti
}()
// Consumer — range berhenti saat channel ditutup
for v := range ch {
fmt.Println(v) // 0 1 2 3 4
}
break dan continue
#
break — Keluar dari Loop
#
break menghentikan loop sepenuhnya dan melanjutkan eksekusi setelah blok loop:
// Cari elemen pertama yang memenuhi kondisi
target := 7
angka := []int{3, 1, 4, 1, 5, 9, 2, 6, 7, 3}
found := -1
for i, v := range angka {
if v == target {
found = i
break // hentikan segera setelah ditemukan
}
}
if found >= 0 {
fmt.Printf("Ditemukan di index %d\n", found)
}
continue — Lewati Iterasi Saat Ini
#
continue melewatkan sisa body loop untuk iterasi saat ini dan langsung ke iterasi berikutnya:
// Proses hanya elemen yang memenuhi kriteria
for i := 0; i < 10; i++ {
if i%2 == 0 {
continue // lewati bilangan genap
}
fmt.Println(i) // hanya mencetak bilangan ganjil: 1 3 5 7 9
}
// Filter elemen dalam slice
data := []string{"apel", "", "mangga", " ", "jeruk"}
var hasil []string
for _, s := range data {
s = strings.TrimSpace(s)
if s == "" {
continue // lewati string kosong atau hanya whitespace
}
hasil = append(hasil, s)
}
fmt.Println(hasil) // [apel mangga jeruk]
Label untuk Nested Loop #
Ketika for bersarang, break dan continue tanpa label hanya mempengaruhi loop terdalam. Untuk mempengaruhi loop luar, gunakan label:
// Tanpa label — break hanya keluar dari loop dalam
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if j == 1 {
break // hanya keluar dari loop j
}
fmt.Printf("(%d,%d) ", i, j)
}
}
// Output: (0,0) (1,0) (2,0) — loop i tetap berjalan
fmt.Println()
// Dengan label — break keluar dari loop berlabel (loop luar)
outer:
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if i == 1 && j == 1 {
break outer // keluar dari loop i sekaligus
}
fmt.Printf("(%d,%d) ", i, j)
}
}
// Output: (0,0) (0,1) (0,2) (1,0) — berhenti saat i=1, j=1
fmt.Println()
// continue dengan label — lanjut ke iterasi berikutnya loop berlabel
outer2:
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if j == 1 {
continue outer2 // langsung ke iterasi i berikutnya
}
fmt.Printf("(%d,%d) ", i, j)
}
}
// Output: (0,0) (1,0) (2,0) — j=1 dan j=2 dilewati karena continue outer2
Gotcha: Closure dalam Loop #
Ini adalah bug klasik yang sangat umum di Go — salah satu yang paling sering ditanyakan di interview dan ditanyakan di Stack Overflow. Ketika kamu membuat closure (fungsi anonim) di dalam loop, closure itu berbagi variabel loop yang sama, bukan menyimpan salinan nilainya:
// ANTI-PATTERN: semua goroutine mencetak nilai yang sama
funcs := make([]func(), 3)
for i := 0; i < 3; i++ {
funcs[i] = func() {
fmt.Println(i) // i adalah variabel yang SAMA untuk semua closure
}
}
for _, f := range funcs {
f()
}
// Output: 3 3 3 (bukan 0 1 2!)
// Karena pada saat f() dipanggil, loop sudah selesai dan i = 3
// SOLUSI 1: buat salinan variabel per iterasi (shadowing)
for i := 0; i < 3; i++ {
i := i // variabel lokal baru yang meng-shadow i loop
funcs[i] = func() {
fmt.Println(i) // capture i yang berbeda setiap iterasi
}
}
// SOLUSI 2: lewatkan sebagai argumen fungsi
for i := 0; i < 3; i++ {
funcs[i] = func(n int) func() {
return func() { fmt.Println(n) }
}(i) // i dievaluasi saat ini, nilai-nya disalin ke n
}
// SOLUSI 3 (paling umum untuk goroutine): lewat parameter
for i := 0; i < 3; i++ {
go func(n int) {
fmt.Println(n) // n adalah salinan per pemanggilan
}(i)
}
Mulai Go 1.22 (Februari 2024), perilaku loop variable telah berubah: setiap iterasi loop membuat variabel baru, sehingga masalah closure di atas tidak terjadi lagi secara default untuk kode yang dikompilasi dengan Go 1.22+. Namun penting memahami pola ini untuk membaca dan memaintain kode Go versi lama.
Pola Idiomatik #
Loop dengan Index Terbalik #
s := []int{1, 2, 3, 4, 5}
// Iterasi dari belakang ke depan
for i := len(s) - 1; i >= 0; i-- {
fmt.Println(s[i])
}
// Output: 5 4 3 2 1
// Hapus elemen dari slice sambil iterasi — harus dari belakang
for i := len(s) - 1; i >= 0; i-- {
if s[i]%2 == 0 {
s = append(s[:i], s[i+1:]...) // hapus elemen genap
}
}
Retry Loop dengan Exponential Backoff #
func withRetry(maxAttempts int, fn func() error) error {
var lastErr error
for attempt := 1; attempt <= maxAttempts; attempt++ {
lastErr = fn()
if lastErr == nil {
return nil // berhasil
}
if attempt == maxAttempts {
break // jangan tidur setelah percobaan terakhir
}
// Exponential backoff: 1s, 2s, 4s, ...
waitTime := time.Duration(1<<uint(attempt-1)) * time.Second
fmt.Printf("Attempt %d gagal, retry dalam %v: %v\n",
attempt, waitTime, lastErr)
time.Sleep(waitTime)
}
return fmt.Errorf("gagal setelah %d percobaan: %w", maxAttempts, lastErr)
}
Sliding Window #
// Hitung rata-rata bergerak (moving average) dengan window size 3
func movingAverage(data []float64, window int) []float64 {
if len(data) < window {
return nil
}
result := make([]float64, 0, len(data)-window+1)
sum := 0.0
// Hitung sum untuk window pertama
for i := 0; i < window; i++ {
sum += data[i]
}
result = append(result, sum/float64(window))
// Geser window: tambah elemen baru, hapus elemen lama
for i := window; i < len(data); i++ {
sum += data[i] - data[i-window]
result = append(result, sum/float64(window))
}
return result
}
Two-Pointer Pattern #
// Cek apakah slice adalah palindrome
func isPalindrome(s []int) bool {
left, right := 0, len(s)-1
for left < right {
if s[left] != s[right] {
return false
}
left++
right--
}
return true
}
fmt.Println(isPalindrome([]int{1, 2, 3, 2, 1})) // true
fmt.Println(isPalindrome([]int{1, 2, 3, 4, 5})) // false
Batch Processing #
// Proses data dalam batch untuk menghindari memory spike
func processBatch(items []Item, batchSize int, fn func([]Item) error) error {
for i := 0; i < len(items); i += batchSize {
end := i + batchSize
if end > len(items) {
end = len(items) // batch terakhir mungkin lebih kecil
}
batch := items[i:end]
if err := fn(batch); err != nil {
return fmt.Errorf("gagal proses batch %d-%d: %w", i, end, err)
}
fmt.Printf("Selesai batch %d-%d (%d items)\n", i, end, len(batch))
}
return nil
}
Perbandingan dengan Bahasa Lain #
Untuk developer yang datang dari bahasa lain, berikut pemetaan pola loop yang familiar ke Go:
// JAVA/C#: while
while (condition) { body }
// GO:
for condition { body }
// JAVA/C#: do-while
do { body } while (condition);
// GO: tidak ada padanan langsung — gunakan:
for {
body
if !condition { break }
}
// PYTHON: for item in list
for item in items:
body
// GO:
for _, item := range items { body }
// JAVA: for (int i = 0; i < n; i++)
for (int i = 0; i < n; i++) { body }
// GO:
for i := 0; i < n; i++ { body }
// PYTHON: enumerate
for i, item in enumerate(items):
body
// GO:
for i, item := range items { body }
// PYTHON: zip
for a, b in zip(list1, list2):
body
// GO: tidak ada zip bawaan — gunakan index
for i := 0; i < len(list1) && i < len(list2); i++ {
a, b := list1[i], list2[i]
body
}
Contoh Program Lengkap #
Program berikut mensimulasikan sistem analitik sederhana yang memproses data penjualan menggunakan berbagai pola loop:
package main
import (
"fmt"
"math"
"sort"
"strings"
)
type Sale struct {
Product string
Category string
Amount float64
Month int
}
// Hitung statistik dasar menggunakan berbagai pola loop
func analyzesSales(sales []Sale) {
if len(sales) == 0 {
fmt.Println("Tidak ada data penjualan")
return
}
// ── Pola 1: Classic for — hitung total
total := 0.0
for i := 0; i < len(sales); i++ {
total += sales[i].Amount
}
// ── Pola 2: for-range — kumpulkan per kategori
perKategori := make(map[string]float64)
for _, s := range sales {
perKategori[s.Category] += s.Amount
}
// ── Pola 3: for-range map dengan sort untuk output konsisten
fmt.Printf("=== Analitik Penjualan ===\n")
fmt.Printf("Total Pendapatan: Rp%.0f\n\n", total)
kategori := make([]string, 0, len(perKategori))
for k := range perKategori {
kategori = append(kategori, k)
}
sort.Strings(kategori)
fmt.Println("Penjualan per Kategori:")
for _, k := range kategori {
pct := perKategori[k] / total * 100
bar := strings.Repeat("█", int(pct/5))
fmt.Printf(" %-15s Rp%8.0f %5.1f%% %s\n",
k, perKategori[k], pct, bar)
}
// ── Pola 4: for-range dengan continue — filter data
fmt.Println("\nPenjualan > Rp1.000.000:")
count := 0
for _, s := range sales {
if s.Amount <= 1_000_000 {
continue // lewati yang kecil
}
fmt.Printf(" %-20s Rp%.0f\n", s.Product, s.Amount)
count++
}
if count == 0 {
fmt.Println(" (tidak ada)")
}
// ── Pola 5: for-range dengan break — cari penjualan terbesar
sort.Slice(sales, func(i, j int) bool {
return sales[i].Amount > sales[j].Amount
})
fmt.Println("\nTop 3 Penjualan Terbesar:")
for i, s := range sales {
if i >= 3 {
break // hanya ambil 3 teratas
}
fmt.Printf(" %d. %-20s Rp%.0f\n", i+1, s.Product, s.Amount)
}
// ── Pola 6: loop terbalik — tampilkan 3 penjualan terkecil
fmt.Println("\nTop 3 Penjualan Terkecil:")
for i := len(sales) - 1; i >= len(sales)-3 && i >= 0; i-- {
rank := len(sales) - i
fmt.Printf(" %d. %-20s Rp%.0f\n", rank, sales[i].Product, sales[i].Amount)
}
// ── Pola 7: nested loop dengan label — cari pola per bulan per kategori
bulan := []int{1, 2, 3}
kats := []string{"Elektronik", "Fashion"}
fmt.Println("\nPenjualan per Bulan dan Kategori:")
outer:
for _, bln := range bulan {
for _, kat := range kats {
subtotal := 0.0
for _, s := range sales {
if s.Month == bln && s.Category == kat {
subtotal += s.Amount
}
}
if subtotal > 0 {
fmt.Printf(" Bulan %d | %-15s Rp%.0f\n", bln, kat, subtotal)
}
}
if bln == 2 {
fmt.Println(" (hanya menampilkan data s/d bulan 2)")
break outer
}
}
// ── Pola 8: moving average untuk tren
perBulan := make([]float64, 13) // index 1-12 untuk bulan 1-12
for _, s := range sales {
if s.Month >= 1 && s.Month <= 12 {
perBulan[s.Month] += s.Amount
}
}
windowSize := 3
fmt.Printf("\nMoving Average %d Bulan:\n", windowSize)
for i := windowSize; i <= 12; i++ {
sum := 0.0
for j := i - windowSize + 1; j <= i; j++ {
sum += perBulan[j]
}
avg := sum / float64(windowSize)
if avg > 0 {
fmt.Printf(" Bulan %2d (avg %d bln): Rp%.0f\n", i, windowSize, avg)
}
}
// ── Pola 9: infinite loop untuk simulasi retry
fmt.Println("\nSimulasi Koneksi Database:")
maxRetry := 3
connected := false
attempt := 0
for {
attempt++
// Simulasi: berhasil di percobaan ke-2
if attempt == 2 {
connected = true
fmt.Printf(" Attempt %d: berhasil terhubung!\n", attempt)
break
}
fmt.Printf(" Attempt %d: gagal, retry...\n", attempt)
if attempt >= maxRetry {
fmt.Println(" Menyerah setelah", maxRetry, "percobaan")
break
}
}
_ = connected
// ── Statistik akhir
amounts := make([]float64, len(sales))
for i, s := range sales {
amounts[i] = s.Amount
}
mean := total / float64(len(sales))
variance := 0.0
for _, a := range amounts {
diff := a - mean
variance += diff * diff
}
variance /= float64(len(sales))
stddev := math.Sqrt(variance)
fmt.Printf("\nStatistik:\n")
fmt.Printf(" Jumlah transaksi: %d\n", len(sales))
fmt.Printf(" Rata-rata : Rp%.0f\n", mean)
fmt.Printf(" Std Deviasi : Rp%.0f\n", stddev)
}
func main() {
sales := []Sale{
{"Laptop Pro", "Elektronik", 15_000_000, 1},
{"Kaos Polos", "Fashion", 250_000, 1},
{"Earphone BT", "Elektronik", 1_500_000, 1},
{"Celana Jeans", "Fashion", 450_000, 2},
{"Monitor 4K", "Elektronik", 5_000_000, 2},
{"Jaket Hoodie", "Fashion", 380_000, 2},
{"SSD 1TB", "Elektronik", 1_200_000, 3},
{"Kemeja Formal", "Fashion", 320_000, 3},
{"Keyboard Mech", "Elektronik", 2_500_000, 3},
{"Topi Baseball", "Fashion", 150_000, 3},
}
analyzesSales(sales)
}
Ringkasan #
- Hanya satu keyword
for— Go tidak punyawhile,do-while, atauforeach; semua pola perulangan diekspresikan denganfor.- Tiga bentuk
for: classic tiga komponen (init; cond; post), condition-only (setarawhile), dan infinite loop (for {}).for-rangemengembalikan index/key dan value untuk slice, array, map, string, dan channel.rangememberikan salinan value — modifikasivtidak mempengaruhi koleksi asli; gunakan index untuk modifikasi.rangepada string mengiterasi per rune (karakter Unicode), bukan per byte; index bisa melompat untuk karakter multi-byte.- Urutan
rangepada map tidak dijamin — sort keynya jika butuh output konsisten.breakdancontinuedengan label untuk mengontrol loop luar dari dalam nested loop.- Closure dalam loop berbagi variabel yang sama — buat salinan dengan
i := iatau lewatkan sebagai argumen untuk menghindari bug.- Go 1.22+ mengubah semantik loop variable sehingga closure dalam loop tidak lagi mengalami masalah ini secara default.
- Infinite loop dengan
for {}adalah pola yang sah untuk server, daemon, dan worker — hentikan denganbreak,return, atau signal dari channel.