Tarkibga o'tish

select operatori

select bir nechta kanal amali orasidan bajarishga tayyor bo‘lgan bittasini tanlaydi. U goroutinelardan kelayotgan natijalarni kutish, vaqt chegarasini qo‘yish va ishni bekor qilish signalini kuzatishda ishlatiladi.

Oddiy kanal amali faqat bitta yuborish yoki qabul qilishni kutadi. select esa bir vaqtning o‘zida bir nechta imkoniyatni kutishga yordam beradi. Masalan, backend xizmat tashqi API javobini kutishi bilan birga so‘rov bekor qilinganini ham kuzatishi mumkin.

Asosiy sintaksis

select {
case msg := <-messages:
    fmt.Println("Qabul qilindi:", msg)
case results <- value:
    fmt.Println("Natija yuborildi")
default:
    fmt.Println("Hozircha tayyor kanal yo‘q")
}

Har bir case faqat kanalga yuborish yoki kanaldan qabul qilish amalini kutadi. select bajarilganda quyidagi qoidalar ishlaydi:

  • faqat bitta case tayyor bo‘lsa, o‘sha case bajariladi;
  • bir nechta case tayyor bo‘lsa, Go ulardan birini psevdotasodifiy tanlaydi;
  • hech biri tayyor bo‘lmasa va default bo‘lmasa, joriy goroutine bloklanadi;
  • hech biri tayyor bo‘lmasa va default bo‘lsa, default darhol bajariladi.

caselarning yozilish tartibi ularga ustuvorlik bermaydi. select kanallarni yuqoridan pastga qarab tekshirib, “birinchi” kanalni tanlamaydi.

Birinchi ishlaydigan misol

package main

import (
    "fmt"
    "time"
)

func main() {
    fast := make(chan string)
    slow := make(chan string)

    go func() {
        time.Sleep(100 * time.Millisecond)
        fast <- "tez natija"
    }()

    go func() {
        time.Sleep(200 * time.Millisecond)
        slow <- "sekin natija"
    }()

    select {
    case value := <-fast:
        fmt.Println(value)
    case value := <-slow:
        fmt.Println(value)
    }
}

Natija:

tez natija

Ikkala kanal ham buffersiz. Shu sababli yuboruvchi goroutine mos qabul qiluvchi tayyor bo‘lmaguncha kutadi. fast taxminan 100 millisekunddan keyin tayyor bo‘ladi va uning casei bajariladi.

Bu misoldagi uyqu vaqtlari turlicha bo‘lgani uchun odatda fast tanlanadi. Ammo operatsiyalar bir paytda tayyor bo‘lsa, natijani caselar tartibiga qarab oldindan aytib bo‘lmaydi.

select faqat bir marta bajarildi. main tugagach, Go boshqa goroutinelarning yakunlanishini kutmaydi. Shu sababli bu dastur ikkinchi natijani qabul qilmaydi.

Bir nechta natijani qabul qilish

Har ikkala natija kerak bo‘lsa, selectni kerakli miqdorda takrorlash mumkin:

package main

import (
    "fmt"
    "time"
)

func main() {
    first := make(chan string)
    second := make(chan string)

    go func() {
        time.Sleep(100 * time.Millisecond)
        first <- "birinchi kanal"
    }()

    go func() {
        time.Sleep(200 * time.Millisecond)
        second <- "ikkinchi kanal"
    }()

    for i := 0; i < 2; i++ {
        select {
        case value := <-first:
            fmt.Println(value)
        case value := <-second:
            fmt.Println(value)
        }
    }
}

Natija:

birinchi kanal
ikkinchi kanal

Sikl selectni ikki marta bajaradi. Birinchi qabuldan keyin u yana kutadi va qolgan kanaldagi qiymatni oladi. Noma’lum miqdordagi natijalarni qabul qilishda sanash o‘rniga kanallarni yopish holatini kuzatish kerak.

Bloklanish nima?

Buffersiz kanalga yuborish uchun qabul qiluvchi, qabul qilish uchun esa yuboruvchi tayyor bo‘lishi kerak. Aks holda joriy goroutine bloklanadi, ya’ni shu amal bajarilguncha kutadi.

Quyidagi kod main goroutine ichida yuboradi, lekin qabul qiluvchi yo‘q:

package main

func main() {
    ch := make(chan int)
    ch <- 10 // Qabul qiluvchi yo‘q, shu sababli runtime deadlockni aniqlaydi.
}

Bu ataylab noto‘g‘ri misol. Dastur fatal error: all goroutines are asleep - deadlock! xatosi bilan tugaydi. Kanalni bufferlash yoki qabul qiluvchi goroutineni ishga tushirish muammoni hal qilishi mumkin, lekin yechim ma’lumot oqimiga qarab tanlanadi.

Bloklanmaydigan kanal amali

default hech bir kanal tayyor bo‘lmaganda kutmasdan davom etish imkonini beradi:

package main

import "fmt"

func main() {
    ch := make(chan int, 1)

    select {
    case value := <-ch:
        fmt.Println("Qabul qilindi:", value)
    default:
        fmt.Println("Kanalda qiymat yo‘q")
    }
}

Natija:

Kanalda qiymat yo‘q

Kanal bufferida qiymat yo‘q, shuning uchun qabul qilish tayyor emas va default bajariladi. Bu usul telemetriya kabi tashlab yuborilishi mumkin bo‘lgan ma’lumotda foydali. Ammo muhim vazifalarni default orqali indamay tashlab yuborish ma’lumot yo‘qolishiga olib keladi.

Warning

for ichida faqat select va default ishlatib, hech qanday kutish qilinmasa, bo‘sh aylanish (busy loop) yuz beradi. Bunday sikl CPU vaqtini bekorga sarflaydi.

Vaqt chegarasi

Tashqi xizmat yoki uzoq hisoblashni cheksiz kutish yaxshi yechim emas. Timer kanalini selectga qo‘shib, operatsiyaga vaqt chegarasi berish mumkin:

package main

import (
    "fmt"
    "time"
)

func main() {
    result := make(chan string, 1)

    go func() {
        time.Sleep(200 * time.Millisecond)
        result <- "tayyor"
    }()

    timer := time.NewTimer(100 * time.Millisecond)
    defer timer.Stop()

    select {
    case value := <-result:
        fmt.Println(value)
    case <-timer.C:
        fmt.Println("vaqt tugadi")
    }
}

Natija:

vaqt tugadi

Hisoblash 200 millisekund davom etadi, timer esa 100 millisekundda signal yuboradi. Shuning uchun timeout casei tanlanadi. result bufferli qilingan: main timeout sababli qabul qilishdan chiqib ketganda ham yuboruvchi goroutine qiymatni joylab, tugay oladi.

Bitta qisqa kutish uchun time.After ham ishlaydi. Uzoq siklda har aylanishda time.After yaratish yangi timerlar hosil qiladi. Bunday holatda time.NewTimerni kerakli paytda Reset qilish yoki davriy signal uchun time.NewTicker ishlatish resurslarni aniqroq boshqaradi.

context orqali bekor qilish

Real backend kodida bekor qilish signali ko‘pincha context.Context orqali uzatiladi. ctx.Done() yopilganda undan qabul qilish darhol tayyor bo‘ladi:

Context yaratish, timeout, deadline va cancel() qoidalari context asoslari darsida batafsil tushuntiriladi.

package main

import (
    "context"
    "fmt"
    "time"
)

func work(ctx context.Context) error {
    ticker := time.NewTicker(20 * time.Millisecond)
    defer ticker.Stop()

    for step := 1; step <= 10; step++ {
        select {
        case <-ctx.Done():
            return ctx.Err()
        case <-ticker.C:
            fmt.Println("qadam:", step)
        }
    }

    return nil
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
    defer cancel()

    if err := work(ctx); err != nil {
        fmt.Println("ish to‘xtadi:", err)
    }
}

Natijadagi qadamlar soni scheduler va vaqt aniqligiga qarab farq qilishi mumkin. Yakuniy satr quyidagicha bo‘ladi:

ish to‘xtadi: context deadline exceeded

Worker har signalda bitta qadam bajaradi. Vaqt chegarasi tugaganda ctx.Done() yopiladi, funksiya ctx.Err()ni qaytaradi. HTTP so‘rovi bekor qilinganda, database chaqiruvi to‘xtatilganda va bir nechta goroutine birgalikda yakunlanishi kerak bo‘lganda shu usul ishlatiladi.

Yopilgan va nil kanal

Yopilgan kanaldan qabul qilish bloklanmaydi. Bufferdagi qiymatlar tugagach, kanal element turining nol qiymatini qaytaradi. Kanal yopilganini ikkinchi ok qiymati bilan tekshirish kerak:

value, ok := <-ch
if !ok {
    // Kanal yopilgan.
}

Sikl ichidagi selectda yopilgan kanal doim tayyor turadi. Uni o‘chirib qo‘yish uchun kanal o‘zgaruvchisiga nil beriladi. nil kanalga yuborish ham, undan qabul qilish ham abadiy bloklanganligi sababli uning casei tanlanmaydi:

package main

import "fmt"

func main() {
    left := make(chan int, 1)
    right := make(chan int, 1)
    left <- 10
    right <- 20
    close(left)
    close(right)

    for left != nil || right != nil {
        select {
        case value, ok := <-left:
            if !ok {
                left = nil
                continue
            }
            fmt.Println("left:", value)
        case value, ok := <-right:
            if !ok {
                right = nil
                continue
            }
            fmt.Println("right:", value)
        }
    }
}

Natija satrlarining tartibi o‘zgarishi mumkin:

left: 10
right: 20

Har bir kanal yopilganda tegishli o‘zgaruvchi nil qilinadi. Ikkalasi ham nil bo‘lgach sikl tugaydi. nilga o‘tkazilmasa, yopilgan kanal qayta-qayta tanlanib, nol qiymat qaytarishi mumkin.

Bo‘sh select

select {}

case va defaultsiz select joriy goroutineni abadiy bloklaydi. mainni sun’iy ushlab turish uchun bundan foydalanish mumkin, ammo amaliy dasturda goroutinelarning tugashini sync.WaitGroup, kanal yoki context orqali boshqarish aniqroq. Aks holda fon vazifasining xatosi yoki tugashi nazoratsiz qoladi.

Keng tarqalgan xatolar

  • select barcha tayyor caselarni bajaradi deb o‘ylash. Har bajarilishda faqat bittasi tanlanadi; ko‘p natija uchun sikl kerak.
  • Yuqoridagi case ustun deb hisoblash. Bir nechta tayyor amal orasidagi tanlov psevdotasodifiy.
  • Yopilgan kanaldagi ok qiymatini tekshirmaslik. Bu nol qiymatlarni haqiqiy ma’lumot deb qabul qilishga olib kelishi mumkin.
  • defaultni har joyda qo‘llash. U kutishni yo‘q qiladi va producer yoki consumerga tabiiy qarshi bosim (backpressure) berilishiga to‘sqinlik qilishi mumkin.
  • Timeoutdan keyin yuboruvchi goroutineni unutish. Qabul qiluvchi ketgan bo‘lsa, buffersiz kanalga yuboruvchi abadiy bloklanishi mumkin.
  • Kanalni qabul qiluvchi tomonda yopish. Odatda boshqa qiymat yuborilmasligini biladigan yuboruvchi kanalni yopadi; yopilgan kanalga yuborish panic keltiradi.

Interviewda muhim nuqtalar

  • select har bajarilganda ko‘pi bilan bitta caseni tanlaydi.
  • defaultsiz select tayyor amal bo‘lmasa bloklanadi, default bilan esa bloklanmaydi.
  • Yopilgan kanal qabul qilish uchun doim tayyor, nil kanal esa hech qachon tayyor emas.
  • select {} abadiy bloklanadi.
  • Timeout operatsiyani kutishni to‘xtatadi, lekin ishni bajarayotgan goroutineni o‘z-o‘zidan bekor qilmaydi. Buning uchun context yoki alohida signal kerak.

Xulosa

select bir nechta kanal amali, timeout va bekor qilish signalini bitta boshqaruv nuqtasida birlashtiradi. Uning asosiy qoidalari — bitta tayyor caseni tanlash, default orqali bloklanmaslik va yopilgan hamda nil kanallarning farqini bilish. Keyingi darsda shu vositalar yordamida goroutinelar sonini cheklaydigan worker pool tuzilishini ko‘rib chiqamiz.