Go 經典入門系列 24:Select

2021-02-19 Go語言中文網

歡迎來到 Golang 系列教程[1]的第 24 篇。

什麼是 select?

select 語句用於在多個發送/接收信道操作中進行選擇。select 語句會一直阻塞,直到發送/接收操作準備就緒。如果有多個信道操作準備完畢,select 會隨機地選取其中之一執行。該語法與 switch 類似,所不同的是,這裡的每個 case 語句都是信道操作。我們好好看一些代碼來加深理解吧。

示例
package main

import (
    "fmt"
    "time"
)

func server1(ch chan string) {
    time.Sleep(6 * time.Second)
    ch <- "from server1"
}
func server2(ch chan string) {
    time.Sleep(3 * time.Second)
    ch <- "from server2"

}
func main() {
    output1 := make(chan string)
    output2 := make(chan string)
    go server1(output1)
    go server2(output2)
    select {
    case s1 := <-output1:
        fmt.Println(s1)
    case s2 := <-output2:
        fmt.Println(s2)
    }
}

在線運行程序[2]

在上面程序裡,server1 函數(第 8 行)休眠了 6 秒,接著將文本 from server1 寫入信道 ch。而 server2 函數(第 12 行)休眠了 3 秒,然後把 from server2 寫入了信道 ch。

而 main 函數在第 20 行和第 21 行,分別調用了 server1 和 server2 兩個 Go 協程。

在第 22 行,程序運行到了 select 語句。select 會一直發生阻塞,除非其中有 case 準備就緒。在上述程序裡,server1 協程會在 6 秒之後寫入 output1 信道,而server2 協程在 3 秒之後就寫入了 output2 信道。因此 select 語句會阻塞 3 秒鐘,等著 server2 向 output2 信道寫入數據。3 秒鐘過後,程序會輸出:

from server2

然後程序終止。

select 的應用

在上面程序中,函數之所以取名為 server1 和 server2,是為了展示 select 的實際應用。

假設我們有一個關鍵性應用,需要儘快地把輸出返回給用戶。這個應用的資料庫複製並且存儲在世界各地的伺服器上。假設函數 server1 和 server2 與這樣不同區域的兩臺伺服器進行通信。每臺伺服器的負載和網絡時延決定了它的響應時間。我們向兩臺伺服器發送請求,並使用 select 語句等待相應的信道發出響應。select 會選擇首先響應的伺服器,而忽略其它的響應。使用這種方法,我們可以向多個伺服器發送請求,並給用戶返回最快的響應了。:)

默認情況

在沒有 case 準備就緒時,可以執行 select 語句中的默認情況(Default Case)。這通常用於防止 select 語句一直阻塞。

package main

import (
    "fmt"
    "time"
)

func process(ch chan string) {
    time.Sleep(10500 * time.Millisecond)
    ch <- "process successful"
}

func main() {
    ch := make(chan string)
    go process(ch)
    for {
        time.Sleep(1000 * time.Millisecond)
        select {
        case v := <-ch:
            fmt.Println("received value: ", v)
            return
        default:
            fmt.Println("no value received")
        }
    }

}

在線運行程序[3]

上述程序中,第 8 行的 process 函數休眠了 10500 毫秒(10.5 秒),接著把 process successful 寫入 ch 信道。在程序中的第 15 行,並發地調用了這個函數。

在並發地調用了 process 協程之後,主協程啟動了一個無限循環。這個無限循環在每一次迭代開始時,都會先休眠 1000 毫秒(1 秒),然後執行一個 select 操作。在最開始的 10500 毫秒中,由於 process 協程在 10500 毫秒後才會向 ch 信道寫入數據,因此 select 語句的第一個 case(即 case v := <-ch:)並未就緒。所以在這期間,程序會執行默認情況,該程序會列印 10 次 no value received。

在 10.5 秒之後,process 協程會在第 10 行向 ch 寫入 process successful。現在,就可以執行 select 語句的第一個 case 了,程序會列印 received value: process successful,然後程序終止。該程序會輸出:

no value received
no value received
no value received
no value received
no value received
no value received
no value received
no value received
no value received
no value received
received value:  process successful

死鎖與默認情況
package main

func main() {
    ch := make(chan string)
    select {
    case <-ch:
    }
}

在線運行程序[4]

上面的程序中,我們在第 4 行創建了一個信道 ch。我們在 select 內部(第 6 行),試圖讀取信道 ch。由於沒有 Go 協程向該信道寫入數據,因此 select 語句會一直阻塞,導致死鎖。該程序會觸發運行時 panic,報錯信息如下:

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:
main.main()
    /tmp/sandbox416567824/main.go:6 +0x80

如果存在默認情況,就不會發生死鎖,因為在沒有其他 case 準備就緒時,會執行默認情況。我們用默認情況重寫後,程序如下:

package main

import "fmt"

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

在線運行程序[5]

以上程序會輸出:

default case executed

如果 select 只含有值為 nil 的信道,也同樣會執行默認情況。

package main

import "fmt"

func main() {
    var ch chan string
    select {
    case v := <-ch:
        fmt.Println("received value", v)
    default:
        fmt.Println("default case executed")

    }
}

在線運行程序[6]

在上面程序中,ch 等於 nil,而我們試圖在 select 中讀取 ch(第 8 行)。如果沒有默認情況,select 會一直阻塞,導致死鎖。由於我們在 select 內部加入了默認情況,程序會執行它,並輸出:

default case executed

隨機選取

當 select 由多個 case 準備就緒時,將會隨機地選取其中之一去執行。

package main

import (
    "fmt"
    "time"
)

func server1(ch chan string) {
    ch <- "from server1"
}
func server2(ch chan string) {
    ch <- "from server2"

}
func main() {
    output1 := make(chan string)
    output2 := make(chan string)
    go server1(output1)
    go server2(output2)
    time.Sleep(1 * time.Second)
    select {
    case s1 := <-output1:
        fmt.Println(s1)
    case s2 := <-output2:
        fmt.Println(s2)
    }
}

在線運行程序[7]

在上面程序裡,我們在第 18 行和第 19 行分別調用了 server1 和 server2 兩個 Go 協程。接下來,主程序休眠了 1 秒鐘(第 20 行)。當程序控制到達第 21 行的 select 語句時,server1 已經把 from server1 寫到了 output1 信道上,而 server2 也同樣把 from server2 寫到了 output2 信道上。因此這個 select 語句中的兩種情況都準備好執行了。如果你運行這個程序很多次的話,輸出會是 from server1 或者 from server2,這會根據隨機選取的結果而變化。

請在你的本地系統上運行這個程序,獲得程序的隨機結果。因為如果你在 playground 上在線運行的話,它的輸出總是一樣的,這是由於 playground 不具有隨機性所造成的。

這下我懂了:空 select
package main

func main() {
    select {}
}

在線運行程序[8]

你認為上面代碼會輸出什麼?

我們已經知道,除非有 case 執行,select 語句就會一直阻塞著。在這裡,select 語句沒有任何 case,因此它會一直阻塞,導致死鎖。該程序會觸發 panic,輸出如下:

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [select (no cases)]:
main.main()
    /tmp/sandbox299546399/main.go:4 +0x20

本教程到此結束。祝你愉快。

上一教程 - 緩衝信道和工作池

下一教程 - Mutex[9]

via: https://golangbot.com/select/

作者:Nick Coghlan[10]譯者:Noluye[11]校對:polaris1119[12]

本文由 GCTT[13] 原創編譯,Go 中文網[14] 榮譽推出

參考資料[1]

Golang 系列教程: https://studygolang.com/subject/2

[2]

在線運行程序: https://play.golang.org/p/3_yaJSoSpG

[3]

在線運行程序: https://play.golang.org/p/8xS5r9g1Uy

[4]

在線運行程序: https://play.golang.org/p/za0GZ4o7HH

[5]

在線運行程序: https://play.golang.org/p/Pxsh_KlFUw

[6]

在線運行程序: https://play.golang.org/p/IKmGpN61m1

[7]

在線運行程序: https://play.golang.org/p/vJ6VhVl9YY

[8]

在線運行程序: https://play.golang.org/p/u8hErIxgxs

[9]

Mutex: https://studygolang.com/articles/12598

[10]

Nick Coghlan: https://golangbot.com/about/

[11]

Noluye: https://github.com/Noluye

[12]

polaris1119: https://github.com/polaris1119

[13]

GCTT: https://github.com/studygolang/GCTT

[14]

Go 中文網: https://studygolang.com/

相關焦點

  • 圖解 Go Select 語句的執行順序
    本文基於 Go 1.14select 允許在一個 goroutine 中管理多個 channel。但是,當所有 channel 同時就緒的時候,go 需要在其中選擇一個執行。此外,go 還需要處理沒有 channel 就緒的情況,我們先從就緒的 channel 開始。
  • Go 經典入門系列 32:panic 和 recover
    panic 和 recover歡迎來到 Golang 系列教程[1]的第 32 篇。什麼是 panic? 在 Go 語言中,程序中一般是使用錯誤[2]來處理異常情況。對於程序中出現的大部分異常情況,錯誤就已經夠用了。
  • Go 經典入門系列 28:Defer
    歡迎來到 Golang 系列教程[1]的第 29 篇。什麼是 defer? defer 語句的用途是:含有 defer 語句的函數,會在該函數將要返回之前,調用另一個函數。這個定義可能看起來很複雜,我們通過一個示例就很容易明白了。
  • Go 經典入門系列 26:結構體取代類​
    歡迎來到 Golang 系列教程[1]的第 26 篇。Go 支持面向對象嗎? Go 並不是完全面向對象的程式語言。Go 官網的 FAQ[2] 回答了 Go 是否是面向對象語言,摘錄如下。可以說是,也可以說不是。
  • Go 經典入門系列 10:switch 語句
    這是 Golang 系列教程 中的第 10 篇。switch 是一個條件語句,用於將表達式的值與可能匹配的選項列表進行比較,並根據匹配情況執行相應的代碼塊。它可以被認為是替代多個 if else 子句的常用方式。
  • Go 經典入門系列 30:錯誤處理
    歡迎來到 Golang 系列教程[1]的第 30 篇。什麼是錯誤? 錯誤表示程序中出現了異常情況。比如當我們試圖打開一個文件時,文件系統裡卻並沒有這個文件。這就是異常情況,它用一個錯誤來表示。error}func (e *PathError) Error() string { return e.Op + " " + e.Path + ": " + e.Err.Error() }如果你有興趣了解上述原始碼出現的位置,可以在這裡找到:https://golang.org/src/os/error.go
  • 經典巧板系列:七巧板入門基礎問題004
    平面幾何是小學到初中階段都要學習的重要知識,七巧板則是經典的中國傳統智力遊戲,玩好七巧板有助於孩子學習平面幾何知識,對於培養數學的思維有也一定的幫助。從今天(9月16日)開始,我們推出《經典巧板系列:七巧板入門基礎問題》,該系列共40期,每期1-3個與七巧板相關的基礎問題,所有問題由益智遊戲方面的專家武元元老師提供。
  • 經典巧板系列:七巧板入門基礎問題003
    平面幾何是小學到初中階段都要學習的重要知識,七巧板則是經典的中國傳統智力遊戲,玩好七巧板有助於孩子學習平面幾何知識,對於培養數學的思維有也一定的幫助。從今天(9月16日)開始,我們推出《經典巧板系列:七巧板入門基礎問題》,該系列共40期,每期1-3個與七巧板相關的基礎問題,所有問題由益智遊戲方面的專家武元元老師提供。
  • 經典巧板系列:七巧板入門基礎問題002
    平面幾何是小學到初中階段都要學習的重要知識,七巧板則是經典的中國傳統智力遊戲,玩好七巧板有助於孩子學習平面幾何知識,對於培養數學的思維有也一定的幫助。從今天(9月16日)開始,我們推出《經典巧板系列:七巧板入門基礎問題》,該系列共40期,每期1-3個與七巧板相關的基礎問題,所有問題由益智遊戲方面的專家武元元老師提供。
  • [GO語言基礎] 一.為什麼我要學習Golang以及GO語言入門普及
    這系列文章入門部分將參考「尚矽谷」韓順平老師的視頻和書籍《GO高級編程》,詳見參考文獻,並結合作者多年的編程經驗進行學習和豐富,且看且珍惜吧!後續會結合網絡安全進行GO語言實戰深入,加油~這些年我學過各種程式語言,從最早的C語言到C++,再到VB、C#、PHP、JAVA,再到IOS開發、Python,到最新的GO語言,學得是真的雜。
  • GitHub開源中文版《Go入門指南》學習教程
    Go 適合寫工具,比如 hugo 、hub、fzf,還有國人寫的 linux 下的百度 pan client 都是 go 實現的。3. Go 適合實現 C/C++ 一部分業務,Java 的大部分業務。4.
  • GO語言入門(第一個go程序)
    本文節選自《go入門指南》GO語言介紹指導設計原則
  • 快速轉型golang(go語言)web開發 01系列概覽
    為什麼要出這個快速轉型go語言的系列?因為現在go語言在國內實在是太火了……火,就意味著有錢途^_^(是的你沒看錯,就是你想的那個錢途)Go在國內到底有多火?現在市面上的大廠:華為、阿里巴巴、騰訊、百度、拼多多、京東、字節跳動、小米、美團、滴滴、360……已經沒有不用go語言的了……但是……go火爆速度遠大於市場上go工程師的供給速度,面對市面上大量go語言的崗位需求和明朗的就業前景,必定會有很多朋友有快速轉型的需求……而且……現在市面上好多go語言的視頻教程都是時長動不動就幾十個小時
  • 玩轉Mysql系列 - 第6篇:select查詢基礎篇
    這是Mysql系列第6篇。環境:mysql5.7.25,cmd命令中進行演示。DQL(Data QueryLanguage):數據查詢語言,通俗點講就是從資料庫獲取數據的,按照DQL的語法給資料庫發送一條指令,資料庫將按需求返回數據。DQL分多篇來說,本文屬於第1篇。
  • SQL系列:當INSERT遇到SELECT...
    CREATE TABLE 新表ASSELECT * FROM 舊一個表複製數據,然後把數據插入到另一個新表中拷貝表結構和數據2SELECT INTO一個表複製數據,然後把數據插入到另一個新表中MySql 資料庫不支持,可用序號1來實現實測PL/SQL單條語句執行不可3INSERT INTO SELECT把select
  • 線稿速寫|簡單好畫的新手入門系列經典動漫人物角色速寫
    線稿速寫|簡單好畫的新手入門系列經典動漫人物角色速寫 畫師:eys.artwork 圖/網絡侵刪
  • 各大牌經典入門級首飾合集
    經典入門級大牌首飾匯總|選首飾必看1.TIFFANY 蒂芙尼來自美國的世界著名的珠寶品牌,T系列微笑項鍊真的是最經典的一款!完全可以作為他們家入門級項鍊!經久耐看,百搭又有標誌性。愛心鑰匙等也都是熱門款式,他們家藍色的包裝盒也好看都不捨得扔的那種!3.
  • 英語入門對話185|For here or to go?
    Eating here or to go? 內用還是外帶?For here or to go? 內用還是外帶?It is a takeout restaurant. 這是外帶餐廳。You can get free refills for your coke. 您的可樂可以免費續杯。
  • Go 性能分析工具 pprof 入門
    通過 runtime/pprof 實現go test: 通過 go test -bench ./go/1.9.1/libexec/src/runtime/sigqueue.go:131 +0xa7os/signal.loop() /usr/local/Cellar/go/1.9.1/libexec/src/os/signal/signal_unix.go:22 +0x22created by os/signal.init.0 /usr/local/Cellar/go/1.9.1/
  • 學習MySQL的select語句
    select語句可 以用回車分隔$sql="select * from article where id=1"和  $sql="select * from article where id=1"批量查詢數據可以用in 來實現 $sql="select * from article where id  ;in(1,3,5)"使用concat連接查詢的結果$sql="select concat(id,"-",con)  as res from article where id=1"返回 "1-article content"