Channel nima?
Go dasturlash tilining eng kuchli xususiyatlaridan biri bu — goroutinlar o'rtasida muloqotni ta'minlovchi "channel" mexanizmidir.
Ko'pgina dasturlash tillari parallel ishlov berish uchun umumiy xotira (Shared Memory) modelini qo'llaydi. Go esa boshqacha yo'l tutadi: CSP (Communicating Sequential Processes) modelini amalga oshiradi.
Go'ning asosiy falsafasi:
Do not communicate by sharing memory; instead, share memory by communicating.
Channel yaratish va ishlatish
Channel'ni make() funksiyasi orqali yaratasiz:
// Buferlanmagan channel (int turi)
ch := make(chan int)
// Buferli channel (3 ta joy)
ch := make(chan int, 3)
// String uchun channel
ch := make(chan string)
// Bo'sh signal channel (faqat signal uchun)
ch := make(chan struct{})Channel yuborish bilan ishlashda faqat bitta operatsiya belgisi ishlatiladi: <-
ch <- 42 // channel'ga 42 yuborish (arrow channelga qaragan)
val := <-ch // channel'dan qabul qilish (arrow channeldan chiqayapti)
<-ch // qabul qilish, lekin qiymatni saqlamaslikfunc main() {
ch := make(chan int) // channel yaratildi
// goroutine orqali yuborish
go func() {
ch <- 42 // 42 ni kanalga yuboradi
}()
// qabul qilish (bloklanib kutadi)
val := <-ch
fmt.Println(val) // Natija: 42
}Bu yerda main goroutine val := <-ch qatorida to'xtab, goroutine ma'lumot yuborguncha kutadi. Bu channel'ning sinxronizatsiya kuchi!
Unbuffered Channel
Buferlanmagan channel — ikki goroutine bir vaqtda tayyor bo'lgandagina ishlaydi. Biri ma'lumot yuborsa, ikkinchisi tayyor bo'lguncha birinchisi kutadi.

Yuboruvchi va qabul qiluvchi bir vaqtda tayyor bo'lishi shart. Aks holda birinchi tayyori ikkinchisini kutadi.
func main() {
ch := make(chan int) // buferlanmagan
go func() {
fmt.Println("Yuborishdan oldin...")
ch <- 1 // qabul qiluvchi tayyor bo'lguncha kutadi
fmt.Println("Yuborishdan keyin (blok tugadi)")
}()
time.Sleep(2 * time.Second) // 2 soniya kech turing
fmt.Println("Qabul qilishga tayyor")
val := <-ch // endi qabul qilindi
fmt.Println("Qabul qilindi:", val)
time.Sleep(100 * time.Millisecond)
}Buffered Channel
Buferli channel'da ma'lumotlar vaqtincha saqlanadi. Bufer to'lgunga qadar yuboruvchi bloklanmaydi.


ch := make(chan int, 3) // 3 ta joy bor
ch <- 42 // joy bor, bloklanmaydi
ch <- 17 // joy bor, bloklanmaydi
ch <- 99 // joy bor, bloklanmaydi
// ch <- 100 // XATO! Bufer to'ldi, bloklanadi
val1 := <-ch // 42 (FIFO tartibida)
val2 := <-ch // 17
val3 := <-ch // 99
fmt.Println(val1, val2, val3) // 42 17 99Channel'ni yopish
Channel'ni yopish orqali "endi ma'lumot kelmaydi" signali yuboriladi. Bu ayniqsa for range bilan channel'dan o'qishda muhim.
Yopish va tekshirish
ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3
close(ch) // channel yopildi
// ok tekshiruvi
val, ok := <-ch
fmt.Println(val, ok) // 1 true (qiymat bor)
val, ok = <-ch
fmt.Println(val, ok) // 2 true
val, ok = <-ch
fmt.Println(val, ok) // 3 true
val, ok = <-ch
fmt.Println(val, ok) // 0 false (channel yopiq, bo'sh)
For range bilan
ch := make(chan int, 5)
for i := 1; i <= 5; i++ {
ch <- i
}
close(ch) // yopmasa for range cheksiz kutadi!
for val := range ch {
fmt.Println(val) // 1, 2, 3, 4, 5 chiqadi keyin to'xtaydi
}Channel'ni faqat yuboruvchi yopishi kerak! Qabul qiluvchi yopsa — yuboruvchi panic bo'ladi. Yopilgan channel'ga yuborish ham panic beradi.
Select operatori
select — Go'dagi channel uchun maxsus konstruktsiya. Bir vaqtda bir nechta channel'ni kuzatib, birinchi tayyor bo'lganini tanlaydi.

Asosiy select misoli
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() { ch1 <- "ch1 dan keldi" }()
go func() { ch2 <- "ch2 dan keldi" }()
for i := 0; i < 2; i++ {
select {
case msg1 := <-ch1:
fmt.Println(msg1)
case msg2 := <-ch2:
fmt.Println(msg2)
}
}
}Ikkita channel bir vaqtda tayyor bo'lsa, select tasodifiy birini tanlaydi. Bu Go runtime tomonidan kafolatlangan xulq-atvordir.
Default bilan (non-blocking)
select {
case val := <-ch:
fmt.Println("Qabul qilindi:", val)
default:
fmt.Println("Hech narsa yo'q, davom etamiz") // bloklanmaydi
}Timeout bilan
select {
case val := <-ch:
fmt.Println("Qabul qilindi:", val)
case <-time.After(3 * time.Second):
fmt.Println("3 soniya o'tdi, timeout!") // vaqt tugadi
}
Xatolardan saqlanish
Deadlock — o'lik qulf
Deadlock: barcha goroutinlar bir-birini kutganda dastur qotib qoladi.
// XATO: Deadlock!
func main() {
ch := make(chan int)
ch <- 1 // main goroutine qotib qoladi — qabul qiluvchi yo'q!
// hech qachon chiqmaydi
}
// TO'G'RI:
func main() {
ch := make(chan int)
go func() { ch <- 1 }() // boshqa goroutine yuboradi
val := <-ch
fmt.Println(val)
}Yopilgan channel'ga yuborish
func main() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Panic ushlab olindi:", r)
}
}()
ch := make(chan int)
close(ch)
ch <- 1 // panic
}Nil channel
var ch chan int // nil channel
// Nil channel'ga yuborish/qabul — CHEKSIZ bloklanadi
// ch <- 1 // forever blocked
// val := <-ch // forever blocked
// Lekin select'da nil case — har doim o'tkazib yuboriladi
select {
case val := <-ch: // ch nil bo'lsa — bu case hech qachon tanlanmaydi
fmt.Println(val)
default:
fmt.Println("ch nil, o'tdik")
}Goroutine oqishi (Goroutine leak)
Channel'ni yopmaslik goroutinlarning "muzlab qolishi"ga olib keladi — ular endi hech qachon tugamaydi.
// XATO: Yopilmagan channel — goroutine oqishi
func badExample() <-chan int {
ch := make(chan int)
go func() {
for i := 0; ; i++ {
ch <- i
}
// close(ch) hech qachon chaqirilmaydi!
}()
return ch
}
// TO'G'RI: done channel bilan to'xtatish
func goodExample(done <-chan struct{}) <-chan int {
ch := make(chan int)
go func() {
defer close(ch)
for i := 0; ; i++ {
select {
case ch <- i:
case <-done: // to'xtatish signali keldi
return
}
}
}()
return ch
}
Namunaviy Kafe stsenariyasi
Tushuntirishni osonlashtirish uchun Partier, Candier va Stringerlar yuritadigan kafe stsenariyasini ko'ramiz:
- Partier — kassada turadi, mijozlardan buyurtma qabul qiladi
- Candier — oshxonada qahva tayyorlaydi (#1 barista)
- Stringer — oshxonada qahva tayyorlaydi (#2 barista)
Qahva tayyorlash buyurtma qabul qilishga qaraganda ko'proq vaqt oladi. Shu sababli Partier buyurtmalarni channel orqali oshxonaga — Candier va Stringerga — uzatadi.

Unbuffered Channel
Unbuffered channel ichki bufersiz channel. Yaratish: ch := make(chan Order) yoki make(chan Order, 0).
Bu turdagi channelda jo'natish va qabul qilish bir vaqtda sodir bo'lishi kerak — biri tayyor bo'lmaguncha ikkinchisi kutib turadi.
1-holat: Bo'sh kanal — ikkalasi kutmoqda
Channelda hech qanday buyurtma yo'q. Candier ham, Stringer ham bo'sh, yangi buyurtma kelgunicha kutib turishadi.

2-holat: Partier buyurtma jo'natadi
Yangi buyurtma keldi. Partier uni channelga qo'yadi va barista olgunicha kutadi — ya'ni ch <- order da bloklanadi. Candier yoki Strinfer olgunicha Partier yangi buyurtma qabul qila olmaydi.

3-holat: Candier birinchi buyurtmani oldi
Candier va Stringer ikkalasi ham tayyor bo'lganda qaysi biri olishini Go runtime aniqlaydi — bu nondeterministik. Aytaylik, Candier birinchi buyurtmani oldi. Candier qahva tayyorlashga kirishadi, Partier esa blokdan chiqib yangi buyurtma kutishga o'tadi.

4-holat: Ikkinchi buyurtma — yana Candier oldi
Ikkinchi buyurtma keldi. Bu safar ham Candier va Stringer ikkalasi bo'sh (Candier avvalgi buyurtmani tugatdi deb faraz qilamiz). Go runtime yana Candier ni tanladi — nondeterministik. Natija oldingi holat bilan bir xil.

5-holat: Uchinchi buyurtma — Stringer oldi
Uchinchi buyurtma keldi. Endi Candier order2 ni tayyorlayapti, shuning uchun faqat Stringer bo'sh. Stringer order3 ni oladi. Ikkala barista parallel ishlaydi.

6-holat: To'rtinchi buyurtma — Partier bloklanadi
To'rtinchi buyurtma keldi. Candier ham, Stringer ham band. Partier ch <- order4 da bloklanadi — kanal bufersiz bo'lgani uchun kimdir olmagunicha buyurtmani qo'ya olmaydi.
Muhim eslatma: Ko'pchilik make(chan Order) (bufer=0) va make(chan Order, 1) (bufer=1) ni chalkashtirib yuboradi. Bufer=0 da ch <- order4 darhol bajarilmaydi — qabul qiluvchi kutiladi. Bufer=1 da esa darhol bajariladi va Partier davom etishi mumkin.

Buffered Channel
Unbuffered channel ishlaydi, lekin umumiy throughputni cheklaydi. Yechim — buffered channel: ichki navbat (bufer) mavjud bo'lgani uchun jo'natuvchi barcha uyalar to'lmaguncha bloklanmaydi.
ch := make(chan Order, 3) // sig'im = 3
Endi Candier va Stringer band bo'lsalar ham, Partier channelga 3 tagacha buyurtma qo'yib, yangi mijozlarni qabul qilishda davom eta oladi.
Buffered channel ishlash tartibi
- Partier order1 ni qo'yadi — bufer: [order1]. Bloklanmaydi.
- Partier order2 ni qo'yadi — bufer: [order1, order2]. Bloklanmaydi.
- Partier order3 ni qo'yadi — bufer: [order1, order2, order3]. Hamon bloklanmaydi.
- Partier order4 ni qo'ymoqchi — bufer to'lgan, bloklanadi.
Bufer FIFO (First In, First Out) tartibida ishlaydi. Candier yoki Stringer bitta buyurtmani olgach, bufer bo'shaydi va Partier davom etadi.
Buffered channel kafe unumdorligini oshiradi, lekin bufer o'lchami to'g'ri tanlanishi kerak. Juda katta bufer — uzoq kutish vaqti. Ba'zan ortiqcha buyurtmani qabul qilmaslik qabul qila olib vaqtida bajara olmaslikdan yaxshiroqdir.
Channels implementatsiyasi chuqurroq tahlili
Channel’larni implementatsiya qilish haqida gapirganda, biz oddiy usulni — lock'lar bilan himoyalangan navbatni — tasavvur qilishimiz mumkin. Aslida, Go ham shunga o'xshash yondashuvni qo'llaydi, va bu hchan strukturasida mujassamlashgan. Ushbu struktura channel implementatsiyasining markaziy qismidir.
Go'ning channel implementatsiyasi uch asosiy strukturaga asoslanadi: hchan, waitq va sudog.
hchan strukturasi channel'lar ichki ishlash tamoyilining yuragida turadi. U goroutinlar o'rtasida ishonchli muloqotni ta'minlab, kanalning turli xil xatti-harakatlarini boshqarish uchun mo'ljallangan. Har bir maydonning rolini oldinga borganimizda chuqurroq ko'rib chiqamiz.
Keling, hchan strukturasi qanday ko'rinishda ekanligini ko'rib chiqaylik:
type hchan struct {
qcount uint // Navbatdagi umumiy ma'lumotlar soni
dataqsz uint // Channel buferining o'lchami
buf unsafe.Pointer // Ma'lumot elementlari massiviga ko'rsatkich
elemsize uint16 // Har bir elementning o'lchami (element turiga qarab belgilanadi)
closed uint32 // Channel yopilganligini bildiruvchi bayroq
elemtype *_type // Channel'ga yuboriladigan elementlarning turi
sendx uint // Ma'lumot yuborish uchun navbatdagi uyaning indeksi
recvx uint // Ma'lumot qabul qilish uchun navbatdagi uyaning indeksi
recvq waitq // Kutayotgan qabul qiluvchilar navbati
sendq waitq // Kutayotgan yuboruvchilar navbati
lock mutex // Channelni himoya qiluvchi mutex
}Channel'ni ishga tushirish
Channel yaratilganda, Go hchan strukturasini heapda joylashtiradi va unga ko'rsatkich qaytaradi. Demak, channel — bu hchan turidagi o'zgaruvchiga ko'rsatkichdan iborat.
Yuqorida aytilganidek, biz buferlangan channel'ni ishga tushirganimizda, channel uzunligiga teng bufer yaratiladi va elementlarni enqueue/dequeue qilish uchun kutadi. Tasavvur qilingki, biror element enqueue qilinadi — u elementni buferga qo'yadi, so'ngra dequeue jarayoni elementni buferdan oladi.
ch := make(chan int, 3) // O'lcham 3 bo'lgan bo'sh channel yaratadi.
ch <- 3 // channel'ga element qo'shadi va sendx indeksini 1 ga oshiradi.
ch <- 4
ch <- 5 // Uchinchi element; sendx 3 ga yetdi (bufer to'ldi, circular)
v := <-ch // Navbatdan element oladi va recvx indeksini 1 ga oshiradi.
Xabarlar yuborish va qabul qilish jarayonni ochish
Keling, goroutinlar channel'lar orqali qanday muloqot qilishini chuqurroq o'rganaylik va channel'lar ichki mexanizmi bilan tanishamiz.
Quyidagi stsenariyni ko'rib chiqing:
Yuboruvchi (G1) va qabul qiluvchi (G2):
Bizning misolimizda ikkita asosiy goroutine mavjud: G1 (yuboruvchi) va G2 (qabul qiluvchi). G1 topshiriqlar ro'yxatiga ega va ularni channel orqali G2 ga ishlashi uchun yuboradi. Mana soddalashtirilgan kod:
// G1
func main() {
tasks := []string{"task1", "task2", "task3"}
ch := make(chan string, 3)
for _, task := range tasks {
ch <- task
}
go worker(ch)
}
// G2
func worker(ch chan string) {
for {
t := <-ch
process(t)
}
}G1 channel'ga ma'lumot yuborishi:
G1 channel'ga topshiriq yuborganda (bufer bo'sh deb faraz qilganda), Go ishlash muhiti quyidagi amallarni bajaradi:

- Qulflash (lock)
- task₀ nusxasini yaratish va uni buf ga enqueue qilish
- Qulfni bo'shatish va G1 ning davom etishiga ruxsat berish
G2 channel'dan ma'lumot qabul qilishi:
Endi G2 kanaldan ma'lumot qabul qilish uchun rejalashtirilganda, Go ishlash muhiti yuqoridagi amallarni aksincha bajaradi:

- Channel bo'yicha qulflash.
- Buferdan task₀ ni dequeue qilish, nusxa yaratish va uni
vo'zgaruvchisiga yuklash. - Qulflashni bo'shatish va G2 ning davom etishiga ruxsat berish.
Xotira buferining nusxalash mexanizmi xotira xavfsizligini ta'minlaydi. Ikkala goroutine ulashgan yagona xotira — bu mutex bilan himoyalangan hchan strukturasidir. Qolgan barcha narsa, masalan topshiriqlarning o'zi, xotira nusxalash orqali uzatiladi.
Bloklash holati
G1 va G2 ma'lumot almashganidan so'ng, bufersiz channel yoki to'liq buferlangan channel bilan bog'liq stsenariyni o'ylab ko'raylik.
Tasavvur qiling: G2 bitta topshiriqni qayta ishlashga juda ko'p vaqt sarflayapti. Bu G2 ning asenkron channel'dan yangi topshiriqlar qabul qila olmasligiga olib keladi. Boshqa tomondan, G1 esa kanalga yangi topshiriqlar yuborishda davom etadi va bloklanmaydi.
Ammo, agar biz to'liq to'lgan buferlangan channel bilan ishlayotgan bo'lsak, G1 channel'ga yangi topshiriqlar yubora olmaydi. Natijada, G1 ning bajarilishi to'xtaydi yoki bloklanadi — va faqat qabul qilish amali sodir bo'lgach davom etadi.
Endi tushundikki, agar yuboruvchi yoki qabul qiluvchi goroutine tayyor bo'lmasa yoki navbat to'la bo'lsa, goroutinlar bloklanadi. Xo'sh, channel qabul qiluvchi va yuboruvchi goroutinlar haqida qanday xabar topadi? Mana shu yerda hchan'ning recvq va sendq maydonlari ishga kiradi.
Keling, recvq va sendq rollarini o'rganaylik. Bu ikkalasi waitq deb ataladigan muhim ma'lumotlarni o'z ichiga oladigan tur o'zgaruvchilaridir.
sendq — channel'ga ma'lumot yuborishga urinayotgan bloklangan goroutinlarni saqlaydi.
recvq — channel'dan ma'lumot o'qishga urinayotgan bloklangan goroutinlarni saqlaydi.
type waitq struct {
first *sudog // Navbatdagi birinchi sudog ga ko'rsatkich
last *sudog // Navbatdagi oxirgi sudog ga ko'rsatkich
}
// waitq — bu navbatda kutayotgan goroutinlar haqidagi ma'lumotlarni
// o'z ichiga olgan bog'liq ro'yxat (linked list).Sudog nima?
Bu kutish navbatlari kontekstida goroutineni ifodalovchi tuzilma. Uni navbatda kutayotgan goroutinlar haqidagi ma'lumotlarni tashuvchi deb o'ylang. U sudog deb nomlanadi.
Yuqorida aytilganidek, ikki stsenariy yuzaga kelishi mumkin: yuboruvchi qabul qiluvchini kutib bloklanadi yoki qabul qiluvchi yuboruvchini kutib bloklanadi.
Ushbu kodni ko'rib chiqing:
// G1
func main() {
tasks := []string{"task1", "task2", "task3"}
ch := make(chan string, 3)
for _, task := range tasks {
ch <- task
}
// channel'ga task4 yuborilmoqda
ch <- "task4"
go worker(ch)
}
// G2
func worker(ch chan string) {
for {
t := <-ch
process(t)
}
}Yuboruvchi qabul qiluvchini kutib bloklanadi:
Yuqoridagi kodda biz task4 ni channel'ga yuborishga harakat qilmoqdamiz — ammo faol qabul qiluvchi yo'q. Bunday holatda, bloklangan yuboruvchi goroutine G1 sudog strukturasiga o'raladi va channel'ning sendq ga joylashtiriladi.

Quyidagilar sodir bo'ladi:
- Channel o'zi uchun
sudogyaratadi va unihchanstrukturasiningsendqga biriktiradi. Keyin kanal Go ishlash muhitining rejalashtiruvchisini G1 ni to'xtashiga (park) chaqiradi. - Go ishlash muhitining rejalashtiruvchisi G1 ning holatini "running" dan "waiting" ga o'zgartiradi.
- Keyinchalik, qabul qiluvchi yangi topshiriqlar qabul qilishga tayyor bo'lganda, u
sudognisendq'dan chiqaradi va G1 yana ishga rejalashtiriladi.
Qabul qiluvchi yuboruvchini kutib bloklanadi:
Endi qabul qiluvchi (G2) ma'lumot kutayotgan stsenariyni ko'raylik. Bu holatda G2 sudog strukturasiga o'raladi — xuddi yuboruvchining holatiga o'xshab. Ushbu sudog channel'ning recvq ga joylashtiriladi va G2 bloklangani hamda channel'ga ma'lumot yuborilishini kutayotgani ko'rsatiladi.

G1 (yuboruvchi) bajarilish uchun rejalashtirilganda, bloklangan qabul qiluvchi (G2) ni davom ettirish uchun ikki asosiy usul mavjud:
- Enqueue usuli: G1 ma'lumotni kanalning buferiga enqueue qiladi, kutayotgan G2 ni recvq dan chiqaradi va rejalashtiruvchiga G2 ishga tayyor ekanligini bildiradi.
- Optimallashtirilgan nusxalash usuli: G1 topshiriq obyektini to'g'ridan-to'g'ri sudog.elem maydonidan G2 stekiga ajratilgan xotira joyiga nusxalaydi.
Nega G1 task0 ni to'g'ridan-to'g'ri G2 stekiga nusxaladi, enqueue qilishning o'rniga?
Optimallashtirilgan nusxalash usuli odatiy bo'lmagan, ammo samarali yondashuvdir. Har bir goroutine o'z alohida stek maydoniga ega bo'lgani va goroutinlar bir-birining holatiga to'g'ridan-to'g'ri kirmasligi sababli, G1 G2 ning stek ko'rsatkichini to'g'ridan-to'g'ri boshqarishi mumkin. Bu G2 qulflashi va channel'ning buferini o'zgartirish zaruriyatini yo'q qiladi. Ushbu optimizatsiya xotira nusxalash yukini kamaytiradi va umumiy sinxronizatsiya xarajatlarini pasaytirib ishlashni yaxshilaydi.
Qo'shimcha o'rganish:
Manbalar:
https://gobyexample.com/channels
Go Concurrency Visually Explained — Channel
Exploring the Depths of Golang Channels