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 را داخل یک حلقه fori قرار دادیم و گفتیم مقدار 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 ها چندتا داده بواسطه کانال دریافت کنیم و عملیاتی را انجام دهیم یا اینکه اگر قصد داریم که از هر یک از 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")