3.7 آموزش استفاده از select

3.7 آموزش استفاده از select

در زبان گو select همانند switch می باشد که دارای case و default هستش اما یک فرق کلی دارد که به عملکردش برمیگردد. case های select برای عملیات ارسال و دریافت از کانال منتظر می ماند. در کل شما با استفاده از select می توانید از کانال های مختلف اطلاعات ارسال و دریافت کنید و پس از آن برروی آن اطلاعات عملیات انجام دهید.

  • select تا زمانی که یکی از case ها آماده شود بلاک می شود.
  • اگر همزمان چندتا case برای انجام عملیات آماده شود select بصورت تصادفی یکی را انتخاب میکند تا عملیات تکمیل شود.
1select {
2case channel_send_or_receive:
3     //Dosomething
4case channel_send_or_receive:
5     //Dosomething
6default:
7     //Dosomething
8}

select از بین case ها موردی را انتخاب می کند که در آن عملیات ارسال یا دریافت کانال بلاک نشده باشد و آماده اجرا باشد. اگر چند مورد از case ها آماده باشد یکی از آنها بصورت تصادفی انتخاب می شود تا فرآیند را تکمیل کند.

بزارید یک مثال ساده بزنیم :

 1package main
 2
 3import "fmt"
 4
 5func main() {
 6    ch1 := make(chan string)
 7    ch2 := make(chan string)
 8
 9    go goOne(ch1)
10    go goTwo(ch2)
11
12    select {
13    case msg1 := <-ch1:
14        fmt.Println(msg1)
15    case msg2 := <-ch2:
16        fmt.Println(msg2)
17    }
18}
19
20func goOne(ch chan string) {
21    ch <- "From goOne goroutine"
22}
23
24func goTwo(ch chan string) {
25    ch <- "From goTwo goroutine"
26}
1$ go run main.go
2From goOne goroutine

در کد فوق ما ۲ تا کانال تعریف کردیم و کانال ها را به توابع goOne و goTwo پاس دادیم سپس داخل تابع به هرکدام از کانال مقداری ارسال شد. حالا در ادامه بدنه main یک select قرار دادیم که هر یک از case ها منتظر دریافت اطلاعات از کانال مشخص شده‌اش است.

بلافاصله پس از‌اینکه یکی از کانال ها آماده ارسال داده شود، کیس آن اجرا می شود. اگر هر دوی کانال ها همزمان آماده ارسال داده شوند، به‌صورت تصادفی یکی از case ها انتخاب می شود تا عملیات را تکمیل کند.

که در خروجی مقداری که از ch1 آمده را نمایش می دهد. اما اگر بخواهیم خروجی هر دو کانال را ببینیم می توانیم از حلقه استفاده کنیم. به مثال زیر توجه کنید :

 1package main
 2
 3import "fmt"
 4
 5func main() {
 6    ch1 := make(chan string)
 7    ch2 := make(chan string)
 8    go goOne(ch1)
 9    go goTwo(ch2)
10    for i := 0; i < 2; i++ {
11        select {
12        case msg1 := <-ch1:
13            fmt.Println(msg1)
14        case msg2 := <-ch2:
15            fmt.Println(msg2)
16        }
17    }
18}
19
20func goOne(ch chan string) {
21    ch <- "From goOne goroutine"
22}
23
24func goTwo(ch chan string) {
25    ch <- "From goTwo goroutine"
26}
1$ go run main.go
2From goOne goroutine
3From goTwo goroutine

در کد فوق ما select را داخل یک حلقه for قرار دادیم و گفتیم اگر مقدار i کوچکتر از ۲ بود ++i شود. که در هر دو تایم مقدار دریافتی از کانال ها را توانستیم به عنوان خروجی چاپ کنیم.

همانطور که قبلا گفتیم اگر شما داخل select یک case را بزارید که هیچ اطلاعات از کانال دریافت نکند ممکن است برنامه شما کاملا بلاک شود و با خطای deadlock مواجه شوید.

 1package main
 2
 3import "fmt"
 4
 5func main() {
 6    ch1 := make(chan string)
 7    select {
 8    case msg := <-ch1:
 9        fmt.Println(msg)
10    }
11}
1$ go run main.go
2fatal error: all goroutines are asleep - deadlock!

اتفاقی که در کد فوق رخ داد ما یک کانال ایجاد کردیم و سپس داخل select یک case قرار دادیم که منتظر دریافت داده از کانال می باشد. اما چون هیچ داده‌ای به کانال ارسال نمی شود برنامه بطور کلی در همان تیکه از کد بلاک می شود و در نهایت شما با این خطا مواجه خواهید شد.

3.7.1 نحوه کنترل عملیات های کانال با select #

در بالا در خصوص select توضیح دادیم که چه کاربردی هایی دارد اما بذارید توضیحات را تکمیل کنیم. وقتی شما قصد دارید از گوروتین و کانال استفاده کنید در اینجا select نقش خیلی پررنگی در کنترل عملیات کانال ها دارد. اینجاست که بحث همزمانی در زبان گو خیلی زیبا می شود. select می تواند بطور همزمان داده را از کانال دریافت کند و برای اجرا سایر عملیات آماده کند. بنابراین select همراه با کانال و گوروتین خیلی ابزار قدرتمندی برای کنترل و مدیریت همگام سازی و همزمانی می باشد.

3.7.1.1 عملیات ارسال با select #

در زیر یک مثالی زدیم که با استفاده از select داده ای را به کانال میریزیم و سپس آن داده را از کانال دیگر دریافت می کنیم :

 1package main
 2
 3import "fmt"
 4
 5func main() {
 6    ch1 := make(chan string)
 7    ch2 := make(chan string)
 8    go goOne(ch1)
 9    go goTwo(ch2)
10    select {
11
12    case msg1 := <-ch1:
13        fmt.Println(msg1)
14    case ch2 <- "To goTwo goroutine":
15    }
16}
17
18func goOne(ch chan string) {
19    ch <- "From goOne goroutine"
20}
21
22func goTwo(ch chan string) {
23    msg := <-ch
24    fmt.Println(msg)
25}
1$ go run main.go
2To goTwo goroutine

در کد فوق ما با استفاده از یکی از case های select داده‌ای را داخل کانال ریختیم و آن داده را داخل گوروتین تابع goTwo دریافت کردیم و پس آن مقدار دریافتی را چاپ کردیم.

3.7.2 استفاده از default در select #

در زبان گو switch و select می توانند یک default داشته باشند. در اینجا default مربوط select رفتارش همانند default داخل switch می باشد. حالا اگر هر یک از case ها عملیات دریافت یا ارسالی برای اجرا نداشته باشند می توانید با استفاده از default از بلاک شدن برای همیشه جلوگیری کنید. و خیلی مهمه که بدانید وقتی دارید داخل select از default استفاده می کنید select از نوع non-blocking می شود. اگر شما داخل select از default استفاده نکنید ممکن است آن بخش کد شما مسدود شود تا زمانیکه یکی از case ها از کانال داده ای را دریافت کند تا ادامه عملیات صورت گیرد.

به مثال زیر توجه کنید :

 1package main
 2
 3import "fmt"
 4
 5func main() {
 6    ch1 := make(chan string)
 7    select {
 8    case msg := <-ch1:
 9        fmt.Println(msg)
10    default:
11        fmt.Println("Default statement executed")
12    }
13}
1$ go run main.go
2Default statement executed

در کد فوق ما یک کانال ایجاد کردیم و دریافت داده از کانال را داخل یکی از case های select قرار دادیم و پس از آن default را قرار دادیم که از مسدود شدن برنامه جلوگیری کند.

3.7.3 مسدود سازی select با استفاده از timeout #

شما می توانید یک select را با استفاده از timeout بطور موقت تا یک بازه زمانی مسدود کنید. که اینکار توسط تابع After داخل پکیج time صورت میگیرد.

1func After(d Duration) <-chan Time

تابع After یک مدت زمان میگیرد و سپس به عنوان خروجی یک کانال فقط دریافت از نوع Time برمیگرداند.

به مثال زیر توجه کنید :

 1package main
 2
 3import (
 4	"fmt"
 5	"time"
 6)
 7
 8func main() {
 9	ch1 := make(chan string)
10	go goOne(ch1)
11
12	select {
13	case msg := <-ch1:
14		fmt.Println(msg)
15	case <-time.After(time.Second * 1):
16		fmt.Println("Timeout")
17	}
18}
19
20func goOne(ch chan string) {
21	time.Sleep(time.Second * 2)
22	ch <- "From goOne goroutine"
23}
1$ go run main.go
2Timeout

در کد فوق ما در یکی از case های select تابع After را به عنوان کانال دریافت کننده قرار دادیم و سپس مقدار ۱ ثانیه به تابع After پاس دادیم و پس از ۱ ثانیه select از مسدودی خارج شد.

3.7.4 select خالی #

یک select خالی و بدون case می تواند برنامه شما را به‌طور کلی بلاک کند و باعث بروز خطای deadlock شود. اگر select خالی داخل یک گوروتین دیگری قرار گیرد آن گوروتین بطور کلی برای همیشه بلاک خواهد شد اما اگر داخل تابع main قرار دهید باعث بروز deadlock خواهد شد.

1package main
2
3func main() {
4    select {}
5}
1$ go run main.go
2fatal error: all goroutines are asleep - deadlock!

3.7.5 استفاده از select در حلقه بینهایت #

ما می توانیم select را داخل یک حلقه بینهایت قرار دهیم تا برای همیشه از case ها چندتا داده را بواسطه کانال دریافت کنیم و عملیاتی را انجام دهیم. همچنین اگر قصد داریم که آنقدر کیس ها چک شوند تا یکی از کانال ها برای ارسال و یا دریافت داده آماده باشد، می توانیم از این روش استفاده کنیم و در نهایت با return کردن از حلقه بینهایت خارج شویم.

 1package main
 2
 3import (
 4	"fmt"
 5	"time"
 6)
 7
 8func main() {
 9	news := make(chan string)
10	go newsFeed(news)
11
12	printAllNews(news)
13}
14
15func printAllNews(news chan string) {
16	for {
17		select {
18		case n := <-news:
19			fmt.Println(n)
20		case <-time.After(time.Second * 1):
21			fmt.Println("Timeout: News feed finished")
22			return
23		}
24	}
25}
26
27func newsFeed(ch chan string) {
28	for i := 0; i < 2; i++ {
29		time.Sleep(time.Millisecond * 400)
30		ch <- fmt.Sprintf("News: %d", i+1)
31	}
32}
1$ go run main.go
2News: 1
3News: 2
4Timeout: News feed finished

در کد فوق ما یک کانال بافر نشده با نام news ایجاد کردیم و این کانال را داخل گوروتین newsFeed و تابع printAllNews قرار داده‌ایم. تابع newsFeed یک مقداری را به کانال ارسال می کند. و ما داخل تابع printAllNews بواسطه حلقه بینهایت و select دریافت می کنیم و یکی از case های select عملیات timeout را دارد که بعد ۱ ثانیه حلقه را کاملا متوقف کند.

3.7.6 select با یک کانال nil #

معمولا اگر یک کانال nil را برای ارسال یا دریافت داخل case قرار دهید برنامه شما همیشه بلاک می شود. اگر شما داخل یکی از case ها بیاید پس از انجام عملیات مقدار یک کانال را nil بزارید case ای که مقدار داخل کانال را دریافت می کند غیرفعال می شود و به هیچ عنوان دیگر قابل استفاده نخواهد بود. و توسط select آن case کاملا نادیده گرفته خواهد شد و select منتظر دریافت و ارسال داده از سایر case ها خواهد بود.

 1package main
 2
 3import (
 4    "fmt"
 5    "time"
 6)
 7
 8func main() {
 9    news := make(chan string)
10    go newsFeed(news)
11    printAllNews(news)
12}
13
14func printAllNews(news chan string) {
15    for {
16        select {
17        case n := <-news:
18            fmt.Println(n)
19            news = nil
20        case <-time.After(time.Second * 1):
21            fmt.Println("Timeout: News feed finished")
22            return
23        }
24    }
25}
26
27func newsFeed(ch chan string) {
28    for i := 0; i < 2; i++ {
29        time.Sleep(time.Millisecond * 400)
30        ch <- fmt.Sprintf("News: %d", i+1)
31    }
32}
1$ go run main.go
2News: 1
3Timeout: News feed finished

در کد فوق ما داخل case n := <-news پس از اینکه مقدار دریافتی را چاپ کردیم اومدیم مقدار کانال news را برابر nil قرار دادیم. حالا اگر داده‌ای به اون کانال ارسال شود دیگر نمی توانیم دریافت کنیم و select آن case را بطور کلی نادیده میگیرد.

1case n := <-news:
2   fmt.Println(n)
3   news = nil

3.7.7 استفاده از break در select #

شما می توانید break را داخل هر یک از case های select استفاده کنید.

 1import "fmt"
 2
 3func main() {
 4	ch := make(chan string, 1)
 5	ch <- "Before break"
 6
 7	select {
 8	case msg := <-ch:
 9		fmt.Println(msg)
10		break
11		fmt.Println("After break")
12	default:
13		fmt.Println("Default case")
14	}
15}
1$ go run main.go
2Before break

در کد فوق ما با استفاده از break توانستیم select را کاملا متوقف کنیم و برنامه اتمام شود و اگر دقت کرده باشید بعد از break کلمات After break چاپ نشده.

1fmt.Println("After break")