[Golang nâng cao] Select Statement trong Golang

Post on: 2023-03-17 00:36:00 | in: Golang
SELECT được sử dụng để chọn một trong nhiều hoạt động được chạy đồng thời trên nhiều kênh, đợi cho đến khi một trong các hoạt động hoàn thành và sau đó tiếp tục với các lệnh tiếp theo.

Tổng quan

Lệnh Select tương tự như câu lệnh switch, khác biệt là trong Select, mỗi câu lệnh case đợi một thao tác gửi hoặc nhận từ một kênh. Câu lệnh Select sẽ đợi cho đến khi thao tác gửi hoặc nhận hoàn thành trên bất kỳ một trong các câu lệnh case. Nó khác biệt với câu lệnh switch trong cách mà mỗi câu lệnh case sẽ thực hiện thao tác gửi hoặc nhận trên một kênh trong khi ở switch mỗi câu lệnh case là một biểu thức. Vì vậy, câu lệnh select cho phép bạn đợi trên nhiều thao tác gửi và nhận từ các kênh khác nhau. Hai điểm quan trọng cần lưu ý về câu lệnh Select là:

  • Lệnh Select chặn cho đến khi bất kỳ một trong các câu lệnh case sẵn sàng.
  • Nếu nhiều câu lệnh case sẵn sàng, thì nó sẽ chọn một cái ngẫu nhiên và tiếp tục.

Dưới đây là định dạng của câu lệnh select:

select {
case channel_send_or_receive:
     //Dosomething
case channel_send_or_receive:
     //Dosomething
default:
     //Dosomething
}

Lệnh Select sẽ chọn trường hợp mà thao tác gửi hoặc nhận trên một kênh không bị chặn và sẵn sàng để thực hiện. Nếu nhiều trường hợp đều sẵn sàng để thực hiện, thì một trong số chúng sẽ được chọn ngẫu nhiên.

Hãy xem một ví dụ đơn giản. Chúng ta sẽ tìm hiểu về trường hợp mặc định sau trong hướng dẫn này.

package main

import "fmt"

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)

    go goOne(ch1)
    go goTwo(ch2)

    select {
    case msg1 := <-ch1:
        fmt.Println(msg1)
    case msg2 := <-ch2:
        fmt.Println(msg2)
    }
}

func goOne(ch chan string) {
    ch <- "From goOne goroutine"
}

func goTwo(ch chan string) {
    ch <- "From goTwo goroutine"
}

Output

From goOne goroutine

Trong chương trình trên, chúng ta đã tạo hai kênh được truyền vào hai goroutine khác nhau. Sau đó, mỗi goroutine gửi một giá trị đến kênh. Trong lệnh select, chúng ta có hai câu lệnh case. Mỗi một trong hai câu lệnh case đang đợi một thao tác nhận để hoàn thành trên một trong các kênh. Khi bất kỳ thao tác nhận nào hoàn thành trên bất kỳ một trong các kênh, nó sẽ được thực hiện và lệnh select sẽ thoát. Vì vậy, như đã thấy từ đầu ra, trong chương trình trên, nó in ra giá trị nhận được từ một trong các kênh và thoát.

Vì vậy, trong chương trình trên, vì không xác định được thao tác gửi nào sẽ hoàn thành sớm hơn, đó là lý do tại sao bạn sẽ thấy các đầu ra khác nhau nếu bạn chạy chương trình nhiều lần. Hãy xem một chương trình khác nơi chúng ta sẽ đặt thời gian chờ trong goroutine goTwo trước khi gửi giá trị đến kênh ch2. Điều này sẽ đảm bảo rằng thao tác gửi trên ch1 sẽ được thực hiện sớm hơn thao tác gửi đến ch2.

package main

import (
    "fmt"
    "time"
)

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)

    go goOne(ch1)
    go goTwo(ch2)

    select {
    case msg1 := <-ch1:
        fmt.Println(msg1)
    case msg2 := <-ch2:
        fmt.Println(msg2)
    }
}

func goOne(ch chan string) {
    ch <- "From goOne goroutine"
}

func goTwo(ch chan string) {
    time.Sleep(time.Second * 1)
    ch <- "From goTwo goroutine"
}

Output

From goOne goroutine

Trong chương trình trên, trong lệnh select, thao tác nhận trên ch1 được hoàn thành trước và do đó lệnh select sẽ luôn thực hiện câu lệnh case đó, như cũng được thể hiện từ đầu ra.

Cũng có thể chờ đợi thao tác nhận hoàn tất trên cả hai kênh bằng cách sử dụng vòng lặp for xuyên suốt lệnh select. Hãy xem một chương trình cho điều đó.

package main

import "fmt"

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)
    go goOne(ch1)
    go goTwo(ch2)
    for i := 0; i < 2; i++ {
        select {
        case msg1 := <-ch1:
            fmt.Println(msg1)
        case msg2 := <-ch2:
            fmt.Println(msg2)
        }
    }
}

func goOne(ch chan string) {
    ch <- "From goOne goroutine"
}

func goTwo(ch chan string) {
    ch <- "From goTwo goroutine"
}

Output

From goOne goroutine
From goTwo goroutine

Trong chương trình trên, chúng ta đặt một vòng lặp for với độ dài là hai xuyên suốt câu lệnh select. Do đó, câu lệnh select được thực thi hai lần và in ra giá trị nhận được từ mỗi câu lệnh case.

Chúng ta đã đề cập trước đó rằng select có thể bị chặn nếu bất kỳ câu lệnh case nào không sẵn sàng. Hãy xem một ví dụ cho điều đó.

package main

import "fmt"

func main() {
    ch1 := make(chan string)
    select {
    case msg := <-ch1:
        fmt.Println(msg)
    }
}

Output

fatal error: all goroutines are asleep - deadlock!

Trong chương trình trên, chúng ta đã tạo một kênh được đặt tên là ch1. Sau đó, chúng ta nhận giá trị từ kênh này trong câu lệnh select. Vì không có goroutine nào gửi giá trị đến kênh này nên nó dẫn đến một tình trạng mắc kẹt và câu lệnh select bị chặn vô thời hạn. Đó là lý do tại sao nó cho kết quả đầu ra như sau:

fatal error: all goroutines are asleep - deadlock!

Sử dụng câu lệnh select

Câu lệnh select rất hữu ích nếu có nhiều goroutine đang gửi dữ liệu đến nhiều kênh đồng thời. Câu lệnh select có thể nhận dữ liệu đồng thời từ bất kỳ một goroutine nào và thực thi câu lệnh đã sẵn sàng. Vì vậy, select cùng với channels và goroutines trở thành một công cụ rất mạnh mẽ để quản lý đồng bộ hóa và đồng thời hóa.

Select với phép gửi dữ liệu

Cho đến nay, chúng ta đã thấy các ví dụ về phép nhận giá trị trong các câu lệnh case của select. Hãy xem một ví dụ về phép gửi giá trị trong select.

package main

import "fmt"

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)
    go goOne(ch1)
    go goTwo(ch2)
    select {

    case msg1 := <-ch1:
        fmt.Println(msg1)
    case ch2 <- "To goTwo goroutine":
    }
}

func goOne(ch chan string) {
    ch <- "From goOne goroutine"
}

func goTwo(ch chan string) {
    msg := <-ch
    fmt.Println(msg)
}

Output

To goTwo goroutine

Select với trường hợp mặc định

Tương tự như switch, câu lệnh select cũng có thể có một câu lệnh mặc định (default case). Câu lệnh mặc định sẽ được thực thi nếu không có phép gửi hoặc nhận nào được sẵn sàng trên bất kỳ câu lệnh case nào. Vì vậy, một cách nào đó, câu lệnh mặc định ngăn câu lệnh select bị chặn mãi mãi. Một điểm quan trọng cần lưu ý là câu lệnh mặc định khiến cho select trở thành không chặn. Nếu câu lệnh select không chứa một câu lệnh mặc định thì nó có thể bị chặn mãi mãi cho đến khi một phép gửi hoặc nhận nào được sẵn sàng trên bất kỳ câu lệnh case nào. Hãy xem một ví dụ để hiểu rõ hơn.

package main

import "fmt"

func main() {
    ch1 := make(chan string)
    select {
    case msg := <-ch1:
        fmt.Println(msg)
    default:
        fmt.Println("Default statement executed")
    }
}

Output

Default statement executed

Trong chương trình trên, có một câu lệnh select đang chờ một phép nhận trên kênh ch1 và một câu lệnh mặc định. Vì không có goroutine nào đang gửi đến kênh ch1, nên trường hợp mặc định được thực thi và select kết thúc. Nếu không có trường hợp mặc định, select sẽ bị chặn.

Select với thời gian chờ giới hạn

Chức năng đóng băng thời gian chờ đợi trong lệnh select có thể được thực hiện bằng cách sử dụng hàm After() của gói thời gian (time). Dưới đây là chữ ký của hàm After().

func After(d Duration) <-chan Time

Đối với hàm After, nó sẽ đợi trong một khoảng thời gian d và sau đó trả về thời gian hiện tại trên một kênh.

https://golang.org/pkg/time/#Time.After

Hãy xem một chương trình select có thời gian chờ

package main

import (
"fmt"
"time"
)

func main() {
ch1 := make(chan string)
go goOne(ch1)

 select {
case msg := <-ch1:
  fmt.Println(msg)
case <-time.After(time.Second * 1):
  fmt.Println("Timeout")
}
}

func goOne(ch chan string) {
time.Sleep(time.Second * 2)
ch <- "From goOne goroutine"
}

Output

Timeout

In chọn ở trên, chúng ta đang đợi hoạt động nhận trên kênh ch1 hoàn thành. Trong trường hợp khác, chúng ta có thêm một trường hợp sử dụng hàm time.After với khoảng thời gian là 1 giây. Do đó, nguyên tắc chung của lời gọi select này là đợi ít nhất 1 giây cho hoạt động nhận trên kênh ch1 hoàn thành, sau đó trường hợp sử dụng hàm time.After sẽ được thực thi. Chúng ta đã đặt một timeout lớn hơn 1 giây trong hàm goOne và do đó, chúng ta thấy trường hợp sử dụng hàm time.After được thực thi và

Timeout

được in ra

Select rỗng

Chọn khối mà không có trường hợp nào là khối chọn rỗng. Khối chọn rỗng sẽ chặn mãi mãi vì không có trường hợp nào để thực thi. Đó cũng là một trong cách để goroutine đợi mãi mãi. Tuy nhiên, nếu khối chọn rỗng này được đặt trong goroutine chính thì nó sẽ gây ra một deadlock. Hãy xem một chương trình.

package main

func main() {
    select {}
}

Output

fatal error: all goroutines are asleep - deadlock!

Trong chương trình trên, chúng ta có một empty select và do đó nó dẫn đến một deadlock, đó là lý do tại sao bạn thấy đầu ra như sau:

fatal error: all goroutines are asleep - deadlock!

Câu lệnh select với vòng lặp vô hạn bên ngoài

Chúng ta có thể sử dụng vòng lặp for vô hạn bên ngoài câu lệnh select. Điều này sẽ làm cho câu lệnh select được thực thi vô số lần. Vì vậy, khi sử dụng câu lệnh for với vòng lặp vô hạn bên ngoài câu lệnh select, chúng ta cần có một cách để thoát khỏi vòng lặp for. Một trong các trường hợp sử dụng của việc có vòng lặp for vô hạn bên ngoài câu lệnh select có thể là bạn đang chờ đợi nhiều thao tác để nhận trên cùng một kênh trong một khoảng thời gian nhất định. Xem ví dụ dưới đây.

package main

import (
"fmt"
"time"
)

func main() {
news := make(chan string)
go newsFeed(news)

 printAllNews(news)
}

func printAllNews(news chan string) {
for {
  select {
  case n := <-news:
   fmt.Println(n)
  case <-time.After(time.Second * 1):
   fmt.Println("Timeout: News feed finished")
   return
  }
}
}

func newsFeed(ch chan string) {
for i := 0; i < 2; i++ {
  time.Sleep(time.Millisecond * 400)
  ch <- fmt.Sprintf("News: %d", i+1)
}
}

Output

News: 1
News: 2
Timeout: News feed finished

Đoạn chương trình trên tạo ra một channel có tên là news chứa dữ liệu kiểu string. Sau đó, ta chuyển kênh này cho hàm newsfeed, hàm này sẽ đẩy các tin tức vào kênh này. Trong câu lệnh select, ta đang nhận các tin tức từ kênh news. Select statement này được đặt trong một vòng lặp vô hạn, vì vậy select statement sẽ được thực hiện nhiều lần cho đến khi chúng ta thoát khỏi vòng lặp. Ta cũng có câu lệnh time.After với độ trễ 1 giây là một trong các câu lệnh case. Do đó, vòng lặp sẽ nhận tất cả các tin tức từ kênh news trong vòng 1 giây và sau đó thoát.

Câu lệnh select với một nil channel

Khi thực hiện gửi hoặc nhận giá trị trên một kênh nil thì sẽ bị chặn mãi mãi. Vì vậy, một trường hợp sử dụng của việc có một kênh nil trong câu lệnh select là để vô hiệu hóa câu lệnh case đó sau khi hoạt động gửi hoặc nhận được hoàn thành trên câu lệnh case đó. Sau đó, kênh có thể được đặt đơn giản là nil. Câu lệnh case đó sẽ bị bỏ qua khi câu lệnh select được thực thi lại và hoạt động gửi hoặc nhận sẽ được chờ đợi trên câu lệnh case khác. Vì vậy, nó được dùng để bỏ qua câu lệnh case đó và thực thi các câu lệnh case khác.

package main

import (
    "fmt"
    "time"
)

func main() {
    news := make(chan string)
    go newsFeed(news)
    printAllNews(news)
}

func printAllNews(news chan string) {
    for {
        select {
        case n := <-news:
            fmt.Println(n)
            news = nil
        case <-time.After(time.Second * 1):
            fmt.Println("Timeout: News feed finished")
            return
        }
    }
}

func newsFeed(ch chan string) {
    for i := 0; i < 2; i++ {
        time.Sleep(time.Millisecond * 400)
        ch <- fmt.Sprintf("News: %d", i+1)
    }
}

Output

News: 1
Timeout: News feed finished

Chương trình trên tương tự với chương trình chúng ta đã học trước đó về cách sử dụng select trong vòng lặp vô hạn. Sự khác biệt duy nhất là sau khi nhận được tin tức đầu tiên, chúng ta đã vô hiệu hóa case statement bằng cách thiết lập kênh tin tức thành nil.

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

Chính vì vậy chúng ta chỉ nhận được tin tức đầu tiên và sau đó chương trình kết thúc do hết thời gian chờ. Đây là một trong những trường hợp sử dụng của kênh nil trong câu lệnh select.

Từ khóa break trong câu lệnh select.

Dưới đây là ví dụ về từ khóa "break".

import "fmt"

func main() {
ch := make(chan string, 1)
ch <- "Before break"

 select {
case msg := <-ch:
  fmt.Println(msg)
  break
  fmt.Println("After break")
default:
  fmt.Println("Default case")
}
}

Output

Before break

Câu lệnh "break" sẽ kết thúc thực thi câu lệnh nằm bên trong nhất và dòng bên dưới sẽ không được thực thi.

fmt.Println("After break")

Tổng kết

Đó là tất cả về câu lệnh select trong Golang. Hy vọng bạn đã thích bài viết này. Vui lòng chia sẻ phản hồi / cải tiến / lỗi trong phần bình luận.

 
Tag: go Golang nâng cao