14 / 73

Sikllar va iteratsiya

Ko'pchilik tillar sizga sikl yozishning uch yoki to'rt usulini beradi: while, do-while, for, ba'zan o'ziga alohida kalit so'z sifatida yamalgan foreach. Go esa aynan bittasini beradi: for. Shu yagona kalit so'z tildagi barcha sikl ehtiyojlarini qoplaydi — klassik sanoqli sikldan tortib serverning cheksiz qabul siklgacha va map yozuvlari bo'ylab yurishgacha. Bu birlashtirish cheklovdan ko'ra garovga o'xshaydi: bir marta o'rganiladigan va hamma joyda bir xil o'qiladigan bitta moslashuvchan konstruksiya har biri o'z aqliy modelini talab qiladigan to'rtta konstruksiyadan ustun keladi. Ushbu bobda for oladigan shakllar, range Goning ichki to'plamlari bo'ylab qanday yurishi, break, continue va label'lar bajarilishni qanday boshqarishi, hamda Go 1.22 da yaxshi tomonga o'zgargan sikl o'zgaruvchisidagi nozik jihat ko'rib chiqiladi.

for ning shakllari

Eng to'liq ko'rinishida for nuqtali vergul bilan ajratilgan uchta qismni oladi: initsializatsiya ifodasi, shart va post-ifoda.

example.go
for i := 0; i < 10; i++ {
    fmt.Println(i)
}

Initsializatsiya sikl boshlanishidan oldin bir marta ishlaydi. Shart har bir iteratsiyadan oldin, shu jumladan birinchisidan oldin ham tekshiriladi; sikl u true bo'lguncha davom etadi. Post-ifoda har bir iteratsiya tanasidan keyin, shart qayta tekshirilishidan oldin ishlaydi. i sikl bilan chegaralangan: u for ifodasidan oldin mavjud emas va undan keyin yo'qoladi, bu esa sikl hisoblagichlarining atrofdagi kodga sizib chiqishiga yo'l qo'ymaydi.

Initsializatsiya va post qismlarini tashlab yuborsangiz, boshqa tillarda while deb ataladigan shartli siklni olasiz:

example.go
i := 0
for i < 10 {
    fmt.Println(i)
    i++
}

Shartni ham butunlay tashlab yuborsangiz, cheksiz sikl (infinite loop) hosil bo'ladi. U siklni to'xtatish uchun ichkaridagi break, return yoki panic'ga tayanadi:

example.go
for {
    data := readData()
    if data == nil {
        break
    }
    process(data)
}

Ushbu cheksiz sikl shakli doimiy ishlovchi Go dasturlarining asosidir: serverlardagi kiruvchi ulanishlarni qabul qilish (listener loops), worker pool'dagi vazifalarni channel'dan kutish va fondagi xizmatlar aynan shu usulda quriladi.

Uch qismli shaklda faqat bitta o'zgaruvchi bilan cheklanib qolish shart emas. Go initsializatsiya va qadam ifodalarida vergul bilan ajratilgan bir nechta o'zgaruvchilardan foydalanish imkonini beradi (bu ikki ko'rsatkichli — two-pointer algoritmlari uchun juda qulay):

example.go
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
    s[i], s[j] = s[j], s[i]
}

range bilan yurish

Klassik uch qismli shakl indekslarni o'zingiz boshqarmoqchi bo'lganingizda ishlatiladi. Ko'p hollarda esa siz tayyor ma'lumotlar to'plami — slice, massiv, map, satr yoki channel bo'ylab harakatlanasiz. range aynan shu maqsad uchun xizmat qiladi va to'plam turiga qarab eng to'g'ri qiymatlarni qaytaradi.

Slice yoki massiv ustida range har bir elementning indeksi va uning nusxasini (value) beradi:

example.go
numbers := []int{1, 2, 3, 4, 5}

for index, value := range numbers {
    fmt.Printf("index %d, value %d\n", index, value)
}

Faqat indeks kerak bo'lsa: for i := range numbers. Faqat qiymat kerak bo'lsa, indeksni bo'sh identifikator bilan e'tiborsiz qoldiring: for _, value := range numbers.

Map ustida range kalit va qiymat juftligini beradi. Muhim jihati — map bo'yicha iteratsiya tartibi ataylab tasodifiylashtirilgan bo'ladi. Go runtime buni ataylab shunday qilgan: dasturchilar hech qachon kafolatlanmagan tartibga tayanib qolmasliklari hamda xavfsizlik (hash-flooding hujumlari) ta'minlanishi uchun. Agar sizga qat'iy tartiblangan chiqish kerak bo'lsa, avval kalitlarni alohida slice'ga yig'ib, saralashingiz kerak:

example.go
keys := make([]string, 0, len(m))
for k := range m {
    keys = append(keys, k)
}
slices.Sort(keys)

for _, k := range keys {
    fmt.Printf("%s: %d\n", k, m[k])
}

(Go 1.21 da qo'shilgan standart slices.Sort funksiyasi generic tiplar bilan ishlaydi va saralash uchun eng zamonaviy, tezkor standart hisoblanadi — u sort.Strings, sort.Ints va shu kabi eski funksiyalarning o'rinbosari).

Satr ustida range oddiy baytlar bo'yicha emas, balki UTF-8 dekodlash orqali har bir Unicode belgi (rune — int32 muqobil nomi) bo'yicha yuradi:

example.go
s := "Hello, 世界"

for i, r := range s {
    fmt.Printf("byte %d: %c\n", i, r)
}

世 va 界 harflari UTF-8 da har biri 3 baytdan joy olgani sababli, i indeksi 1 tadan emas, 3 tadan sakrab boradi.

range shuningdek channel'lar bilan ham mukammal ishlaydi: u channel yopilgunga (close) qadar undan ma'lumotlarni o'qib boradi, yopilgach esa sikl o'z-o'zidan to'xtaydi:

example.go
ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3
close(ch)

for value := range ch {
    fmt.Println(value)
}

Bu yerda ochiq ok tekshiruvi kerak emas: yopilgan va bo'shatilgan channel shunchaki siklni to'xtatadi, bu esa range ni ishlab chiqaruvchi goroutine chiqishini u tugaganini bildirguncha iste'mol qilishning idiomatik usuliga aylantiradi. 40-bob channel'larni va bu patternni tartib bilan ko'rib chiqadi.

Go 1.22 dan boshlab range oddiy butun sonni ham qabul qiladi, bu juda ko'p uchraydigan "shunchaki N marta sana" sikli uchun kichik, ammo yoqimli qulaylik:

example.go
for i := range 5 {
    fmt.Println(i) // 0, 1, 2, 3, 4
}

U for i := 0; i < 5; i++ ga teng, shunchaki qisqaroq, va faqat son muhim bo'lganda bo'sh identifikator bilan yaxshi birikadi: for range 5 { ... } blokni hech narsa nomlamasdan besh marta ishga tushiradi.

Iteratorlar: funksiyalar ustidan range

Go 1.23 range ni uchinchi marta, tuzilmaviy jihatdan kengaytirdi: endi u funksiya ustidan yura oladi, agar o'sha funksiya standart iter paketi belgilagan ikki aniq shakldan biriga ega bo'lsa — iter.Seq[V] (yield func(V) bool ni qabul qiladigan funksiya) yoki iter.Seq2[K, V] (juftliklar beradi). Bu range ni bir nechta ichki tipga qattiq bog'langan narsa emas, umumiy maqsadli iteratsiya protokoliga aylantiradi va kutubxona mualliflariga slice yoki map ustidan yurishdek his qilinadigan maxsus ketma-ketliklarni taqdim etish imkonini beradi.

example.go
func Count(n int) iter.Seq[int] {
    return func(yield func(int) bool) {
        for i := 0; i < n; i++ {
            if !yield(i) {
                return
            }
        }
    }
}

for i := range Count(5) {
    fmt.Println(i)
}

yield funksiyasi har bir element uchun bir marta chaqiriladi; iste'mol qiluvchi sikl erta chiqib ketgan bo'lsa (break yoki return orqali) u false qaytaradi, bu esa iteratorga qiymat ishlab chiqarishni to'xtatishni va kerak bo'lsa o'zidan keyin tozalashni aytadi. Siz bu shaklni standart kutubxonaning yangiroq qo'shimchalarida tobora ko'proq ko'rasiz: maps va slices paketlaridagi maps.Keys, maps.Values va slices.All oddiy slice emas, shunday iteratorlarni qaytaradi, bu esa shunchaki bir marta yurish uchun oraliq to'plam ajratishdan qutqaradi. O'z iterator funksiyalaringizni yozish hamon chindan unumdorlik yoki birikuvchanlik ehtiyoji bor kutubxona kodiga saqlangan ancha ilg'or qadam — ilova kodi asosan shunchaki slice'lar, map'lar va channel'lar ustidan yuradi — ammo funksiya imzosida iter.Seq ni ko'rganda bu patternni tanib olish asboblaringizda bo'lish foydali.

Bajarilishni boshqarish: break, continue va label'lar

break o'zi turgan eng ichki for, switch yoki select dan darhol chiqadi:

example.go
for i := 0; i < 10; i++ {
    if i == 5 {
        break
    }
    fmt.Println(i)
}
// 0 1 2 3 4 chiqaradi

continue joriy iteratsiya tanasining qolgan qismini o'tkazib yuborib, keyingisiga o'tadi:

example.go
for i := 0; i < 10; i++ {
    if i%2 == 0 {
        continue
    }
    fmt.Println(i)
}
// 1 3 5 7 9 chiqaradi

Ikkalasi ham standart holda faqat o'zini bevosita o'rab turgan siklga ta'sir qiladi, va bu sikllarni ichma-ich joylashtirib, ichkidan turib tashqisidan chiqmoqchi bo'lgan zahoti muammoga aylanadi. Goning javobi — label'lar (labels): siklga biriktirilgan nom, unga break yoki continue ochiq murojaat qila oladi.

example.go
outer:
for i := 0; i < 3; i++ {
    for j := 0; j < 3; j++ {
        if i == 1 && j == 1 {
            break outer
        }
        fmt.Printf("i=%d j=%d\n", i, j)
    }
}

continue outer ham xuddi shunday ishlaydi: ichki siklning emas, tashqi siklning keyingi iteratsiyasiga o'tadi. Label'lar — Go sintaksisining ortiqcha ishlatilsa o'qishga chindan zarar yetkaza oladigan kam sonli qismlaridan biri, shuning uchun ko'pchilik uslub qo'llanmalari (shu jumladan ushbu kitobniki ham) ularni aynan ichma-ich sikl ichidan tashqi siklni boshqarish holati uchun saqlab qo'yadi va niyatni yozib qo'yish foydali bo'lganda umumiy outer: o'rniga tavsiflovchi nomlarni (search:, retry:) afzal ko'radi.

Go 1.22 dagi sikl o'zgaruvchisi inqilobi

O'n yildan ortiq vaqt davomida Go tilida eng ko'p uchraydigan tuzoqlardan biri — sikl ichida closure (anonim funksiya) yoki goroutine ishga tushirilganda sikl o'zgaruvchisining xotiradagi manzilini ushlab qolishi edi:

example.go
var funcs []func()
for i := 0; i < 3; i++ {
    funcs = append(funcs, func() {
        fmt.Println(i)
    })
}
for _, f := range funcs {
    f()
}

Go 1.0 dan to Go 1.21 gacha bu kod 0 1 2 emas, balki 3 3 3 natijasini chiqarar edi! Sababi: i o'zgaruvchisi barcha iteratsiyalar uchun yagona xotira maydonida yashagan. Har bir closure o'sha bitta xotira katagiga havola (pointer) qilib olgan, closure bajarilganda esa sikl allaqachon tugab, i o'zining oxirgi qiymatiga (3) yetib borgan bo'lardi. Ilgari buning chorasi sikl ichida i := i deb yangi o'zgaruvchi yaratish (shadowing qilish) yoki uni argument sifatida uzatish edi.

Go 1.22 versiyasida bu holat til darajasida butunlay to'g'rilandi: endi for sikllarida (oddiy uch qismli shaklda ham, range da ham) har bir iteratsiya uchun alohida yangi o'zgaruvchi xotiradan ajratiladi. Natijada yuqoridagi kod hech qanday qo'shimcha hiylalarsiz tabiiy tarzda 0 1 2 chiqaradi.

Xuddi shu yangilik goroutine'larda yuz beradigan eng xavfli muammo — ma'lumotlar poygasi (data race) xavfini ham bartaraf etdi:

example.go
for i := 0; i < 3; i++ {
    go func() {
        fmt.Println(i) // Go 1.22 dan boshlab har bir goroutine o'z alohida i qiymatini oladi
    }()
}

Ushbu o'zgarish go.mod faylidagi Go versiyasiga bog'liq: agar go.mod da go 1.22 yoki undan yuqori ko'rsatilgan bo'lsa, yangi xavfsiz semantika ishlaydi. 2024-yilgacha yozilgan eski loyihalarda esa i := i qatorini uchratishingiz mumkin — bu ortiqcha kod emas, balki eski Go versiyalaridagi muammoning idiomatik yechimi bo'lgan. Hamon eski semantikada kompilyatsiya bo'ladigan modullarda go vet ning loopclosure tekshiruvi bu pattern kerak bo'lgan joylarni belgilab beradi.

Keng tarqalgan patternlar

Bir nechta sikl shakli shu qadar tez-tez uchraydiki, ularni nomlash foydali — hech bo'lmasa birovning kodida qaytadan o'ylab chiqarish o'rniga ularni bir zumda tanib olish uchun.

Elementni qidirib, topilgan zahoti to'xtash:

example.go
var found *Item
for _, item := range items {
    if item.ID == targetID {
        found = &item
        break
    }
}

"Bu slice'da shartga mos narsa bormi" degan keng tarqalgan holat uchun standart slices paketi endi slices.ContainsFunc va slices.IndexFunc ni taklif qiladi. Ular xuddi shu niyatni bir qatorda ifodalaydi, va qidirish davomida boshqa hech narsa qilish kerak bo'lmasa, siklni qo'lda yozish o'rniga ularga murojaat qilish foydali.

Yangi slice'ga filtrlash:

example.go
var evens []int
for _, v := range numbers {
    if v%2 == 0 {
        evens = append(evens, v)
    }
}

Yig'ilib boradigan natijani to'plash:

example.go
sum := 0
for _, v := range numbers {
    sum += v
}

Va ikki o'lcham bo'ylab ichma-ich yurish, u panjara yoki matritsa shaklidagi har qanday narsada doimo uchraydi:

example.go
for row := 0; row < height; row++ {
    for col := 0; col < width; col++ {
        fmt.Printf("(%d,%d) ", row, col)
    }
    fmt.Println()
}

Bu patternlarning hech biri g'ayrioddiy emas — gap ham shunda. Godagi for sikli deyarli har doim kichik sondagi tanish shakllardan biri kabi o'qiladi, va bir vaqtning o'zida break, continue hamda bir nechta mantiqiy bayroqqa murojaat qilish o'rniga o'z sikllaringizni ham shu shakllardan biri sifatida tanib olinadigan holda saqlash — bu yerda amalda "o'qishga qulay" degani deyarli shu.

Sikl tanasi bir necha qator haqiqiy mantiqdan oshganda, o'sha mantiqni sikl shunchaki chaqiradigan nomli funksiyaga ajratish o'qish qulayligi uchun siklning o'zini qayta qurishdan ko'ra ko'proq foyda beradi.

Sikl va shoxlanish qo'lda bo'lgach, keyingi qadam — ularni Goning to'plam tiplari ustida jiddiy ishlatish. 15-bob massivlar va slice'larga o'tadi: slice ichkarida aslida qanday ifodalanadi, nega bu ifoda uning ham qulayligini, ham o'tkir chetlarini tushuntiradi, va append qachon o'sishni qanday hal qiladi.