Oldingi bobda biz goroutine'larni qanday ishga tushirishni va ular o'rtasida kanallar orqali ma'lumot uzatishni ko'rib chiqdik. Ammo Go kanallari bundan ancha ko'p va qiziqarli imkoniyatlarga ega. Keling, ularni birma-bir o'rganamiz!
Keling, satrni vergullar bo'yicha bo'laklarga ajratadigan va bo'sh qismlarni filtrlab tashlaydigan dasturni ko'rib chiqaylik:
package main
import (
"fmt"
"strings"
)
func main() {
str := "one,two,,four"
in := make(chan string)
go func() { // (1)
words := strings.Split(str, ",")
for _, word := range words {
in <- word
}
}()
for { // (2)
word := <-in
if word != "" {
fmt.Printf("%s ", word)
}
}
}
Natija:
one two four
fatal error: all goroutines are asleep - deadlock!
Goroutine ➊ satrni so'zlarga ajratadi va ularni in kanaliga yuboradi. Sikl ➋ esa kanaldan so'zlarni o'qiydi va bo'sh bo'lmaganlarini ekranga chiqaradi.
Afsuski, dastur muvaffaqiyatsiz tugadi:
fatal error: all goroutines are asleep - deadlock!
Muammo shundaki, sikl ➋ cheksiz ishlaydi:
for {
word := <-in
if word != "" {
fmt.Printf("%s ", word)
}
}
Qayerdan bilish mumkin, in kanalida boshqa so'z qolmaganini va sikldan chiqish vaqti kelganini? Oldingi bobda biz buni bo'sh satr "" orqali tekshirib hal qilgan edik:
for {
word := <-in
if word == "" {
break
}
}
Lekin hozirgi misolimizda bo'sh satr — bu to'g'ri, haqiqiy ma'lumot bo'lishi mumkin (chunki "one,two,,four" satrida ikkita vergul orasida bo'sh satr bor). Uni shunchaki tashlab yuborish kerak, sikldan chiqish signali sifatida qabul qilib bo'lmaydi!
Buni hal qilishning bir yo'li — maxsus belgi (sentinel value) yuborish:
in kanaliga maxsus qiymat yuboradi ➊package main
import (
"fmt"
"strings"
)
const eof = "__EOF__"
func main() {
str := "one,two,,four"
in := make(chan string)
go func() {
words := strings.Split(str, ",")
for _, word := range words {
in <- word
}
in <- eof // (1)
}()
for {
word := <-in
if word == eof { // (2)
break
}
if word != "" {
fmt.Printf("%s ", word)
}
}
fmt.Println()
}
Natija:
one two four
Dastur ishladi, ammo tasavvur qilganingizdek, bu juda zaif va noqulay yechim: agar haqiqiy matn ichida tasodifan "__EOF__" so'zi uchrab qolsa nima bo'ladi? Yaxshiyamki, Go buning uchun maxsus toza mexanizmni taqdim etadi.
Biz concurrent (bir vaqtda ishlovchi) muhitda ikki tomonning o'zaro aloqasidagi klassik muammoga duch keldik:
Go tilida bu muammoni hal qiluvchi rasmiy mexanizm bor:
close) mumkin.Yozuvchi kanalni Go'ning o'rnatilgan close() funksiyasi orqali yopadi:
str := "one,two,,four"
in := make(chan string)
go func() {
words := strings.Split(str, ",")
for _, word := range words {
in <- word
}
close(in) // barcha ma'lumotlar uzatib bo'lindi!
}()
O'quvchi esa kanaldan o'qiyotganda ikkinchi qiymat — "comma OK" orqali kanal holatini tekshiradi:
for {
word, ok := <-in
if !ok {
// kanal yopilgan va bo'sh!
break
}
if word != "" {
fmt.Printf("%s ", word)
}
}
Natija:
one two four
Faraz qilaylik, yozuvchi "one" va "two" satrlarini yubordi va so'ng kanalni yopdi. O'quvchi nimani oladi:
// in <- "one"
// in <- "two"
// close(in)
val, ok := <-in // val: "one", ok: true
val, ok := <-in // val: "two", ok: true
val, ok := <-in // val: "", ok: false
val, ok := <-in // val: "", ok: false
Kanal ochiq bo'lsa, o'quvchi navbatdagi qiymatni va true statusini oladi. Agar kanal yopilgan bo'lsa, o'quvchi tipning nol qiymatini (satrlar uchun "", sonlar uchun 0, pointerlar uchun nil) va false statusini oladi.
Ko'rib turganingizdek, yopilgan kanaldan istalgancha ko'p marta o'qish mumkin — u har doim nol qiymat va false qaytaradi. Bu tasodifiy emas, balki Go tilining qasddan shunday loyihalangan muhim xususiyatidir.
[!WARNING] Kanal faqat bir marta yopilishi kerak. Yopilgan kanalni yana qayta yopish (yoki
nilkanalni yopish) panic keltirib chiqaradi:example.goin := make(chan string) close(in) close(in) // panic: close of closed channel!
for sikli va ok statusi orqali tekshirish juda yaxshi ishlaydi, ammo Go tilida yopiq kanallarni o'qish uchun yanada ixcham va qulay sintaksis mavjud — bu range operatoridir:
package main
import (
"fmt"
"strings"
)
func main() {
str := "one,two,,four"
in := make(chan string)
go func() {
words := strings.Split(str, ",")
for _, word := range words {
in <- word
}
close(in)
}()
for word := range in {
if word != "" {
fmt.Printf("%s ", word)
}
}
fmt.Println()
}
for word := range in sikli kanaldan qiymatlarni bittalab o'qiydi va kanal yopilgach, siklni o'z-o'zidan xavfsiz to'xtatadi. Bu aynan avvalgi word, ok := <-in; if !ok { break } kodi bilan bir xil ishlaydi, biroq ancha toza va o'qilishi oson!
[!IMPORTANT]
rangeorqali kanaldan o'qiyotganingizda, yozuvchi goroutine kanalni albattaclose()qilishi SHART! Aks holda qabul qiluvchi sikl oxirgi qiymatdan keyin yangi qiymat kutib abadiy bloklanadi va dastur deadlock holatiga tushadi.
Berilgan sonlar massividagi faqat juft sonlarni alohida goroutine orqali kanalga chiqaruvchi generateEvens(nums []int) <-chan int funksiyasini yozing.
close).range orqali o'qilganda barcha juft sonlar to'g'ri chiqsin va dastur deadlock'siz yakunlansin.package main
import "fmt"
// generateEvens faqat juft sonlarni kanal orqali yuboradi va kanalni yopadi.
func generateEvens(nums []int) <-chan int {
out := make(chan int)
// TODO: goroutine ishga tushiring, juft sonlarni out kanaliga yuboring va kanalni yoping
return out
}
func main() {
numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
for n := range generateEvens(numbers) {
fmt.Printf("%d ", n)
}
fmt.Println()
}
Keling, oldingi dasturimizni alohida filter funksiyasini ajratib olgan holda qayta tuzaylik:
// filter in kanalidan so'zlarni o'qiydi, bo'sh bo'lmaganlarini out kanaliga yuboradi
func filter(in chan string, out chan string) {
for word := range in {
if word != "" {
out <- word
}
}
close(out)
}
Bu yerda filter funksiyasi ikkita kanal qabul qiladi:
in — faqat o'qish uchun;out — faqat yozish uchun.Ammo hozirgi ko'rinishida har ikkala kanal ikki tomonlama (bidirectional) hisoblanadi. Agar dasturchi yanglishib in kanaliga yozib qo'ysa yoki out kanalidan o'qishga harakat qilsa, Go kompilyatori bunga to'sqinlik qilolmaydi.
Bunday xatolarning oldini olish uchun Go yo'naltirilgan kanallar (directional channels) turini taqdim etadi:
chan<- T — faqat yozish uchun (send-only channel);<-chan T — faqat o'qish uchun (receive-only channel).Strelka <- yozuviga qarab eslab qolish juda oson:
chan<- — qiymat kanalga kirib ketmoqda (yozish);<-chan — qiymat kanaldan chiqib kelmoqda (o'qish).Keling, filter parametrlarini to'g'ri yo'nalishlar bilan belgilaymiz:
func filter(in <-chan string, out chan<- string) {
for word := range in {
if word != "" {
out <- word
}
}
close(out)
}
Endi agar siz adashib in <- "xato" deb yozsangiz, Go kompilyatori kodni ishga tushirishdan oldin to'xtatadi:
invalid operation: cannot send to receive-only channel in (type <-chan string)
Xuddi shuningdek, close() funksiyasini faqat yozish huquqiga ega bo'lgan (chan<-) yoki ikki tomonlama (chan) kanallarda chaqirish mumkin. Qabul qiluvchi (<-chan) kanalni yopishga urinish ham kompilyatsiya xatosidir.
Oddiy ikki tomonlama kanal chan T avtomatik ravishda chan<- T yoki <-chan T turiga o'tadi (implicit conversion). Shuning uchun funksiyalar chaqiruvida hech qanday ortiqcha cast qilish shart emas.
Sonlarni kvadratga ko'taruvchi square(in <-chan int, out chan<- int) funksiyasini yozing. U in kanalidan barcha sonlarni o'qib, har birining kvadratini out kanaliga yuborishi va ish yakunida out kanalini yopishi kerak.
package main
import "fmt"
func square(in <-chan int, out chan<- int) {
// TODO: in kanalidan range orqali o'qing, kvadratini out ga yozing va out ni yoping
}
func main() {
in := make(chan int)
out := make(chan int)
go func() {
defer close(in)
for i := 1; i <= 5; i++ {
in <- i
}
}()
go square(in, out)
for res := range out {
fmt.Printf("%d ", res)
}
fmt.Println()
}
package main
import "fmt"
func square(in <-chan int, out chan<- int) {
defer close(out)
for num := range in {
out <- num * num
}
}
func main() {
in := make(chan int)
out := make(chan int)
go func() {
defer close(in)
for i := 1; i <= 5; i++ {
in <- i
}
}()
go square(in, out)
for res := range out {
fmt.Printf("%d ", res)
}
fmt.Println()
}
Tasavvur qiling, siz bir vaqtda ishlayotgan bir nechta goroutine'larni boshqaryapsiz va ularning barchasiga "Ishni to'xtating!" degan umumiy signal yuborishingiz kerak.
Buni qanday amalga oshirish mumkin? Kanalga qiymat yuborish faqat bitta qabul qiluvchini uyg'otadi. Agar sizda 10 ta goroutine bo'lsa, siz kanalga 10 marta qiymat yuborishingiz kerak bo'ladi.
Lekin biz yuqorida ajoyib xususiyatni o'rgandik: yopilgan kanaldan istalgancha ko'p marta o'qish mumkin va u darhol javob qaytaradi!
Demak, agar biz kanalni close(done) qilsak, ushbu kanalni kutayotgan barcha goroutine'lar bir vaqtning o'zida blokirovkadan chiqadi va to'xtash signali oladi!
Bunday kanal odatda done channel (bajarildi kanali) deb ataladi va uning tipi sifatida chan struct{} tanlanadi, chunki Go tilida bo'sh struct struct{} xotirada 0 bayt joy egallaydi:
package main
import (
"fmt"
"time"
)
func worker(id int, done <-chan struct{}) {
for {
select {
case <-done:
fmt.Printf("Worker #%d: to'xtash signali olindi
", id)
return
default:
// Foydali ish bajarish simulyatsiyasi:
time.Sleep(50 * time.Millisecond)
}
}
}
func main() {
done := make(chan struct{})
// 3 ta ishchini ishga tushiramiz:
for i := 1; i <= 3; i++ {
go worker(i, done)
}
time.Sleep(120 * time.Millisecond)
fmt.Println("Main: barcha ishchilarni to'xtatish signali yuborilmoqda...")
close(done) // hamma ishchilar bir vaqtda signal oladi!
time.Sleep(50 * time.Millisecond)
}
Quyidagi mashqda berilgan soniyadan keyin close(done) chaqirib, cheksiz hisoblab turgan ishchini xavfsiz to'xtating.
package main
import (
"fmt"
"time"
)
func counter(done <-chan struct{}) <-chan int {
out := make(chan int)
go func() {
defer close(out)
n := 1
for {
select {
case <-done:
return
case out <- n:
n++
time.Sleep(20 * time.Millisecond)
}
}
}()
return out
}
func main() {
done := make(chan struct{})
ch := counter(done)
// TODO: 5 ta son olgach done kanalini yoping
for n := range ch {
fmt.Printf("%d ", n)
if n == 5 {
// done kanalini yoping
}
}
fmt.Println("
Tugatildi!")
}
package main
import (
"fmt"
"time"
)
func counter(done <-chan struct{}) <-chan int {
out := make(chan int)
go func() {
defer close(out)
n := 1
for {
select {
case <-done:
return
case out <- n:
n++
time.Sleep(20 * time.Millisecond)
}
}
}()
return out
}
func main() {
done := make(chan struct{})
ch := counter(done)
for n := range ch {
fmt.Printf("%d ", n)
if n == 5 {
close(done)
}
}
fmt.Println("
Tugatildi!")
}
Go tilida buferlanmagan kanallar bilan ishlaganda eng ko'p uchraydigan xato — bu deadlock (boshi berk ko'cha)dir:
func main() {
ch := make(chan int)
ch <- 42 // DEADLOCK!
fmt.Println(<-ch)
}
Bu dastur nima uchun ishlamaydi? Chunki buferlanmagan kanal — bu sinxron uchrashuv nuqtasi (rendezvous). Yuboruvchi ma'lumotni yozish paytida qarama-qarshi tomonda uni qabul qilib oluvchi boshqa goroutine hozir bo'lishini talab qiladi. Bir xil goroutine ichida avval yozib, keyin o'qiyman deyish imkonsiz — birinchi qatordayoq goroutine muzlaydi va dastur halokatga uchraydi!
Deadlock'dan qochish qoidalari:
Buferlanmagan kanalda ch <- x amali qabul qiluvchi kelguncha darhol bloklanadi.
Ammo agar biz kanal yaratayotganda unga sig'im (capacity) ko'rsatsak:
ch := make(chan int, 3)
Bu buferlangan kanal (buffered channel) bo'ladi!
┌─────────────────────────────────┐
│ Bufer: [ 10 ] [ 20 ] [ 30 ] │ Sig'imi: 3 ta element
└─────────────────────────────────┘
Uning ishlash qoidalari:
ch <- x qilganda bloklanmaydi — qiymat buferga qo'yiladi va yuboruvchi o'z ishida davom etadi.len(ch) == cap(ch)) navbatdagi yuboruvchi bloklanadi.<-ch qilganda bloklanmaydi — buferdagi eng birinchi elementni (FIFO) oladi.package main
import "fmt"
func main() {
ch := make(chan string, 2) // 2 ta elementli bufer
ch <- "salom"
ch <- "dunyo"
// Bufer to'ldi, ammo biz hech qanday alohida goroutine ochmadik
// chunki bufer yozuvchini bloklamadi!
fmt.Println(<-ch) // salom
fmt.Println(<-ch) // dunyo
}
JavaScript'dagi Promise.all() kabi, bir nechta mustaqil asinxron operatsiyalarni parallel ishga tushirib, ularning natijalarini buferlangan kanal orqali yig'uvchi funksiya tuzing.
package main
import (
"fmt"
"time"
)
func fetchSource(id int, ch chan<- string) {
time.Sleep(50 * time.Millisecond)
ch <- fmt.Sprintf("Manba #%d dan ma'lumot", id)
}
func main() {
sources := 3
results := make(chan string, sources)
// TODO: 3 ta manba uchun goroutine ishga tushiring va natijalarni results ga yuboring
for i := 1; i <= sources; i++ {
// go fetchSource(...)
}
// TODO: results kanalidan 3 ta natijani o'qib ekranga chiqaring
}
package main
import (
"fmt"
"time"
)
func fetchSource(id int, ch chan<- string) {
time.Sleep(50 * time.Millisecond)
ch <- fmt.Sprintf("Manba #%d dan ma'lumot", id)
}
func main() {
sources := 3
results := make(chan string, sources)
for i := 1; i <= sources; i++ {
go fetchSource(i, results)
}
for i := 1; i <= sources; i++ {
fmt.Println(<-results)
}
}
Boshqa dasturlash tillarida (JS, C#, Python) keng tarqalgan async/await andozasini Go tilida kanallar orqali juda tabiiy va qulay amalga oshirish mumkin:
async funksiya — bu ichida goroutine ishga tushirib, natija uchun kanal qaytaruvchi oddiy funksiya.await — bu o'sha kanaldan qiymat o'qish (<-ch).package main
import (
"fmt"
"time"
)
// downloadAsync ma'lumotni orqa fonda yuklaydi va kanal qaytaradi (async)
func downloadAsync(url string) <-chan string {
out := make(chan string, 1)
go func() {
time.Sleep(100 * time.Millisecond) // yuklash simulyatsiyasi
out <- "Ma'lumot: " + url
}()
return out
}
func main() {
// Async tarzda ikkita yuklashni boshlaymiz:
future1 := downloadAsync("https://golang.org")
future2 := downloadAsync("https://pkg.go.dev")
// Asosiy oqim boshqa ishlar bilan band bo'lishi mumkin...
fmt.Println("Boshqa hisob-kitoblar bajarilmoqda...")
// Endi natijalarni kutamiz (await):
res1 := <-future1
res2 := <-future2
fmt.Println(res1)
fmt.Println(res2)
}
Tizim resurslarini (masalan, ma'lumotlar bazasi ulanishlari sonini yoki parallel yuborilayotgan HTTP so'rovlar sonini) asrash uchun bir vaqtning o'zida ko'pi bilan $N$ ta goroutine ishlashiga ruxsat beruvchi mexanizm kerak bo'ladi. Bu semafor deb ataladi.
Go tilida buferlangan kanal tayyor semafor vazifasini o'taydi:
sem := make(chan struct{}, N) — maksimal $N$ ta ruxsat biletiga ega semafor.sem <- struct{}{} — bilet olish (agar bufer to'lgan bo'lsa, boshqa ishchi chiqib ketguncha kutadi).<-sem — biletni qaytarib topshirish (bo'shatish).package main
import (
"fmt"
"sync"
"time"
)
func main() {
const maxConcurrent = 2 // bir vaqtda ko'pi bilan 2 ta goroutine
sem := make(chan struct{}, maxConcurrent)
var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
sem <- struct{}{} // ruxsat olish
defer func() { <-sem }() // ish tugagach ruxsatni bo'shatish
fmt.Printf("Vazifa #%d boshlandi
", id)
time.Sleep(100 * time.Millisecond)
fmt.Printf("Vazifa #%d tugatildi
", id)
}(i)
}
wg.Wait()
}
5 ta veb-sahifani parallel yuklash kerak, ammo serverni yuklab qo'ymaslik uchun bir vaqtda faqat 2 ta yuklovchi ishlashi mumkin. Yuqoridagi semafor andozasidan foydalanib dasturni to'ldiring.
package main
import (
"fmt"
"sync"
"time"
)
func download(url string, sem chan struct{}, wg *sync.WaitGroup) {
// TODO: semafor orqali cheklov o'rnating, URLni ekranga chiqaring va ruxsatni bo'shating
}
func main() {
urls := []string{"site1.uz", "site2.uz", "site3.uz", "site4.uz", "site5.uz"}
sem := make(chan struct{}, 2)
var wg sync.WaitGroup
for _, u := range urls {
wg.Add(1)
go download(u, sem, &wg)
}
wg.Wait()
fmt.Println("Barcha sahifalar yuklandi!")
}
package main
import (
"fmt"
"sync"
"time"
)
func download(url string, sem chan struct{}, wg *sync.WaitGroup) {
defer wg.Done()
sem <- struct{}{} // bilet olish
defer func() { <-sem }() // biletni qaytarish
fmt.Printf("Yuklanmoqda: %s
", url)
time.Sleep(50 * time.Millisecond)
}
func main() {
urls := []string{"site1.uz", "site2.uz", "site3.uz", "site4.uz", "site5.uz"}
sem := make(chan struct{}, 2)
var wg sync.WaitGroup
for _, u := range urls {
wg.Add(1)
go download(u, sem, &wg)
}
wg.Wait()
fmt.Println("Barcha sahifalar yuklandi!")
}
Buferlangan kanal yopilganda nima yuz beradi? Go juda mantiqiy qoidaga amal qiladi:
panic: send on closed channel).ok == true bilan navbatma-navbat o'qib tugatadi.ok == false qiymati qaytariladi.package main
import "fmt"
func main() {
ch := make(chan int, 3)
ch <- 10
ch <- 20
close(ch) // kanal yopildi, lekin buferda 10 va 20 bor
val, ok := <-ch
fmt.Printf("val: %d, ok: %t
", val, ok) // 10, true
val, ok = <-ch
fmt.Printf("val: %d, ok: %t
", val, ok) // 20, true
val, ok = <-ch
fmt.Printf("val: %d, ok: %t
", val, ok) // 0, false (bufer bo'shadi!)
}
Initsializatsiya qilinmagan (ya'ni make chaqirilmagan) kanal nil qiymatiga ega bo'ladi:
var ch chan int // ch == nil
nil kanal bilan ishlaganda quyidagi g'aroyib, ammo foydali qoidalar amal qiladi:
nil kanalga yozish (ch <- 1) — abadiy bloklaydi!nil kanaldan o'qish (<-ch) — abadiy bloklaydi!nil kanalni yopish (close(ch)) — darhol panic beradi!Bu nimaga kerak?
select operatori bilan ishlaganda, vaqtincha biror kanal bo'yicha tinglashni yoki yozishni to'xtatib qo'yish (disable qilish) kerak bo'lsa, ushbu kanal o'zgaruvchisiga shunchaki nil qiymati beriladi. select operatori nil kanallarni o'z-o'zidan e'tiborsiz qoldiradi (ular hech qachon faollashmaydi). Bu juda ilg'or va qulay usuldir!
Ushbu bobda biz Go kanallarining deyarli barcha jihatlarini to'liq o'rganib chiqdik:
close() va ok tekshiruvi orqali ma'lumot yakunlanganini xavfsiz bildirish;for ... range yordamida kanal bo'ylab avtomatik va toza iteratsiya;chan<- va <-chan yo'naltirilgan kanallari orqali kod xavfsizligini ta'minlash;done kanali orqali ko'plab goroutine'larga bir vaqtda to'xtatish signali yuborish;nil kanallarning o'ziga xos xususiyatlari.Keyingi bob: 03. Pipelines (Kanallar konveyeri)!