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
casetayyor bo‘lsa, o‘shacasebajariladi; - bir nechta
casetayyor bo‘lsa, Go ulardan birini psevdotasodifiy tanlaydi; - hech biri tayyor bo‘lmasa va
defaultbo‘lmasa, joriy goroutine bloklanadi; - hech biri tayyor bo‘lmasa va
defaultbo‘lsa,defaultdarhol 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:
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:
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:
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:
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:
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:
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:
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
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
selectbarcha tayyorcaselarni bajaradi deb o‘ylash. Har bajarilishda faqat bittasi tanlanadi; ko‘p natija uchun sikl kerak.- Yuqoridagi
caseustun deb hisoblash. Bir nechta tayyor amal orasidagi tanlov psevdotasodifiy. - Yopilgan kanaldagi
okqiymatini 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
selecthar bajarilganda ko‘pi bilan bittacaseni tanlaydi.defaultsizselecttayyor amal bo‘lmasa bloklanadi,defaultbilan esa bloklanmaydi.- Yopilgan kanal qabul qilish uchun doim tayyor,
nilkanal 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
contextyoki 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.