channel的高級玩法

單向通道

我們在說“通道”時一般說的都是雙向通道,即:既可以發也可以收的通道。這裏的“發”和“收”是站在操作通道的代碼的角度說的。

所謂單向通道,就是隻能發不能收,或者只能收不能發的通道。

定義單向通道

var uselessChan = make(chan<- int , 1)  //發送通道:只能發不能收

var uselessChan = make(<-chan int , 1)  //接收通道:只能收不能發

在關鍵字chan的前面或後面加上通道收發操作符即可表示。

通道就是爲了傳遞數據而存在的,聲明一個只有一端能用的通道沒有任何意義。

單向通道的用途

概括地講:單向通道最主要的用途就是約束其他代碼的行爲。主要在函數中約束代碼的行爲。

解析

func SendInt(ch chan<- int) {
     ch <- rand.Intn(1000)
}

SendInt函數的參數是一個chan<- int 類型的通道。在這個函數中的代碼就只能向參數ch發送元素值,而不能從ch裏接收元素值。這就起到了約束函數行爲的作用。

在實際場景中:這種約束一般會出現在接口類型聲明的某個方法定義上。

type Notifier interface {
     SendInt(ch chan<- int)
}

我們在接口中定義的方法中如果使用了單向通道類型,那麼就相當於對這個接口的所有實現做了約束。這個約束方式在編寫模板代碼或者可擴展的程序庫的時候很有用。

我們雖然在方法中聲明接收單向通道作爲參數,但是,實際向方法傳遞參數的時候,只需要把一個元素類型匹配的雙向通道傳遞給它就行了,因爲Go語言在這種情況下會自動地把雙向通道轉換爲函數所需的單向通道。

我們還可以在函數聲明的結果列表中使用單向通道。

func getIntChan() <-chan int{
       num := 5
	   ch := make(chan int, num)
	   for i := 0; i<num; i++ {
	         ch <- i
	   }
	   close(ch)
	   return ch
}

函數getIntChan會返回一個<-chan int類型的通道,得到該通道的程序,只能從通道中接收元素值。

這是對函數調用方的約束。

帶range子句的for語句與通道聯用

intChan2 := getIntChan()
for elem := range intChan2 {
	fmt.Printf("The element in intChan2: %v\n", elem)
}

上面的for語句稱爲帶有range子句的for語句。

  • 一、這樣一條for語句會不斷地嘗試從intChan2種取出元素值,即使intChan2被關閉,它也會在取出所有剩餘的元素值之後再結束執行。
  • 二、當intChan2中沒有元素值時,它會被阻塞在有for關鍵字的那一行,直到有新的元素值可取。
  • 三、假設intChan2的值爲nil,那麼它會被永遠阻塞在有for關鍵字的那一行。

select語句與通道聯用

select語句只能與通道聯用,它一般由若干個分支組成。select語句有2中分支:候選分支,以關鍵字case開頭,後面跟一個case表達式和一個冒號,下一行寫要執行的語句;默認分支:以關鍵字default開頭,後面跟一個冒號,下一行寫要執行的語句。

select語句是專門爲通道設計的,每個case表達式中都只能包含操作通道的表達式。

// 準備好幾個通道。
intChannels := [3]chan int{
	make(chan int, 1),
	make(chan int, 1),
	make(chan int, 1),
}
// 隨機選擇一個通道,並向它發送元素值。
index := rand.Intn(3)
fmt.Printf("The index: %d\n", index)
intChannels[index] <- index
// 哪一個通道中有可取的元素值,哪個對應的分支就會被執行。
select {
case <-intChannels[0]:
	fmt.Println("The first candidate case is selected.")
case <-intChannels[1]:
	fmt.Println("The second candidate case is selected.")
case elem := <-intChannels[2]:
	fmt.Printf("The third candidate case is selected, the element is %d.\n", elem)
default:
	fmt.Println("No candidate case is selected!")
}
  • 如果沒有加默認分支,一旦所有的case表達式都沒有滿足求值條件,那麼select語句就會被阻塞。直到至少有一個case表達式滿足條件爲止。
  • 加入了默認分支,無論通道操作的表達式是否阻塞,select語句都不會被阻塞。如果那幾個表達式都阻塞了,或者說都沒有滿足求值的條件,那麼默認分支就會被選擇和執行
  • 前面我們瞭解到:我們可能會因爲通道關閉了,而直接從通道中接收到一個元素類型的零值。所以在很多時候,我們需要通過接收表達式的第二個結果值來判斷通道是否已經關閉。一旦發現一個通道關閉了,我們就應該及時屏蔽掉對應的分支或者採取其他措施。
  • select語句只能對其中的每一個case表達式各求值一次。所以,如果我們想連續或定時地操作其中的通道的話,就往往需要通過在for語句中嵌入select語句的方式實現。但這時要注意,簡單地在select語句的分支中使用break語句,只能結束當前的select語句的執行,而並不會對外層的for語句產生作用。這種錯誤的用法可能會讓這個for語句無休止地運行下去。

select語句的分支選擇規則都有哪些?

規則如下面所示。

  1. 對於每一個case表達式,都至少會包含一個代表發送操作的發送表達式或者一個代表接收操作的接收表達式,同時也可能會包含其他的表達式。比如,如果case表達式是包含了接收表達式的短變量聲明時,那麼在賦值符號左邊的就可以是一個或兩個表達式,不過此處的表達式的結果必須是可以被賦值的。當這樣的case表達式被求值時,它包含的多個表達式總會以從左到右的順序被求值。

  2. select語句包含的候選分支中的case表達式都會在該語句執行開始時先被求值,並且求值的順序是依從代碼編寫的順序從上到下的。結合上一條規則,在select語句開始執行時,排在最上邊的候選分支中最左邊的表達式會最先被求值,然後是它右邊的表達式。僅當最上邊的候選分支中的所有表達式都被求值完畢後,從上邊數第二個候選分支中的表達式纔會被求值,順序同樣是從左到右,然後是第三個候選分支、第四個候選分支,以此類推。

  3. 對於每一個case表達式,如果其中的發送表達式或者接收表達式在被求值時,相應的操作正處於阻塞狀態,那麼對該case表達式的求值就是不成功的。在這種情況下,我們可以說,這個case表達式所在的候選分支是不滿足選擇條件的。

  4. 僅當select語句中的所有case表達式都被求值完畢後,它纔會開始選擇候選分支。這時候,它只會挑選滿足選擇條件的候選分支執行。如果所有的候選分支都不滿足選擇條件,那麼默認分支就會被執行。如果這時沒有默認分支,那麼select語句就會立即進入阻塞狀態,直到至少有一個候選分支滿足選擇條件爲止。一旦有一個候選分支滿足選擇條件,select語句(或者說它所在的 goroutine)就會被喚醒,這個候選分支就會被執行。

  5. 如果select語句發現同時有多個候選分支滿足選擇條件,那麼它就會用一種僞隨機的算法在這些分支中選擇一個並執行。注意,即使select語句是在被喚醒時發現的這種情況,也會這樣做。

  6. 一條select語句中只能夠有一個默認分支。並且,默認分支只在無候選分支可選時纔會被執行,這與它的編寫位置無關。

  7. select語句的每次執行,包括case表達式求值和分支選擇,都是獨立的。不過,至於它的執行是否是併發安全的,就要看其中的case表達式以及分支中,是否包含併發不安全的代碼了

如果在select語句中發現某個通道已關閉,那麼應該怎樣屏蔽掉它所在的分支?

當第二個boolean參數爲false的時候,在相應的case中設置chan爲nil零值,再次case求值的時候會遭遇阻塞,會屏蔽該case

在select語句與for語句聯用時,怎樣直接退出外層的for語句?

通過定義標籤,配合goto或者break能實現在同一個函數內任意跳轉,故可以跳出多層嵌套的循環。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章