Golang語言之管道channel快速入門篇

尹正杰發表於2024-08-05

                                              作者:尹正傑

版權宣告:原創作品,謝絕轉載!否則將追究法律責任。

目錄
  • 一.channel的基本使用
    • 1.channel概述
    • 2.管道入門案例
      • 2.1 有緩衝管道和無緩衝管道概述
      • 2.2 有緩衝管道
      • 2.3 無緩衝管道
    • 3.管道的關閉
      • 3.1 管道關閉操作結果概述
      • 3.2 管道關閉案例
      • 3.3 判斷通道是否關閉
    • 4.管道的遍歷
    • 5.協程和管道協同工作案例
    • 6.宣告只讀只寫的管道
      • 6.1 單向通道概述
      • 6.2 宣告單向管道
      • 6.3 單向管道作為引數傳遞
    • 7.管道的阻塞
      • 7.1 什麼時候會出現管道的阻塞
      • 7.2 只寫不讀緩衝管道滿就會阻塞
      • 7.3 只讀不寫的情況下會阻塞管道
  • 二.select多路複用
    • 1.select語句概述
    • 2.select案例

一.channel的基本使用

1.channel概述

共享記憶體交換資料弊端:
	- 單純地將函式併發執行是沒有意義的。函式與函式間需要交換資料才能體現併發執行函式的意義。
	- 雖然可以使用共享記憶體進行資料交換,但是共享記憶體在不同的goroutine中容易發生競態問題。
	- 為了保證資料交換的正確性,很多併發模型中必須使用互斥量對記憶體進行加鎖,這種做法勢必造成效能問題。


Go語言採用的併發模型是CSP(Communicating Sequential Processes),提倡透過"通訊共享記憶體"而不是透過"共享記憶體實現通訊"。

管道(channel)特質介紹:
	- 管道(channel)是一種特殊的型別,本質就是一個類似"佇列"的資料結構;
	- 管道(channel)像一個傳送帶或者佇列,總是遵循先入先出(First In First Out)的規則,保證收發資料的順序;
	- 管道(channel)自身是執行緒安全,多協程訪問時,不需要加鎖;
	- 管道(channel)是有型別的,一個string的管道只能存放string型別資料;
	- 管道(channel)是可以讓一個goroutine傳送特定值到另一個goroutine的通訊機制;
	
	
channel是Go語言中一種特有的型別。宣告通道型別變數的格式如下:
	var 變數名稱 chan 元素型別

  其中:
    chan:是關鍵字
    元素型別:是指通道中傳遞元素的型別
    
  舉幾個例子:
    var ch1 chan int   // 宣告一個傳遞整型的通道
    var ch2 chan bool  // 宣告一個傳遞布林型的通道
    var ch3 chan []int // 宣告一個傳遞int切片的通道

2.管道入門案例

2.1 有緩衝管道和無緩衝管道概述

宣告的通道型別變數需要使用內建的make函式初始化之後才能使用。具體格式如下:
		make(chan 元素型別, [緩衝大小])

	需要傳遞兩個引數:
		- 第一個引數
	       是channel儲存的資料型別,比如"chan int"表示儲存的是int型別。
	    - 第二個引數:
		   指的是channel的容量大小,channel的緩衝大小是可選的。若不指定預設值為0。
	       若容量為0則無法從中寫入資料,如果非要寫會報錯"fatal error: all goroutines are asleep - deadlock!"

有緩衝管道特點:
	- 1.只要管道的容量大於零,那麼該管道就屬於有緩衝的管道,管道的容量表示管道中最大能存放的元素數量。
	- 2.當管道內已有元素數達到最大容量後,再向管道執行傳送操作就會阻塞,除非有從管道執行接收操作。

無緩衝管道特點:
	- 1.無緩衝的管道又稱為阻塞的管道,單來說就是無緩衝的管道必須有至少一個接收方才能傳送成功。
	- 2.無緩衝的管道只有在有接收方能夠接收值的時候才能傳送成功,否則會一直處於等待傳送的階段。
	- 3.同理,如果對一個無緩衝管道執行接收操作時,沒有任何向管道中傳送值的操作那麼也會導致接收操作阻塞。
	- 4.使用無緩衝管道進行通訊將導致傳送和接收的goroutine同步化,因此,無緩衝管道也被稱為同步管道。

2.2 有緩衝管道

package main

import (
	"fmt"
)

func main() {
	// 1.定義一個int型別的管道
	var intChan chan int

	// 未初始化的通道型別變數其預設零值是nil。
	fmt.Printf("intChan = %v\n", intChan)

	//2.透過make初始化有緩衝管道,管道可以存放3個int型別的資料 
	intChan = make(chan int, 3)

	// 3.管道是引用型別
	fmt.Printf("intChan的值: %v\n", intChan)

	// 4.向管道存放資料
	intChan <- 100
	intChan <- 200
	// 儲存的資料不能大於管道channel容量。
	// intChan <- 300
	// intChan <- 400

	// 5.檢視管道的長度
	fmt.Printf("intChan的實際大小: %d, 容量是: %d\n", len(intChan), cap(intChan))

	// 6.在管道中讀取資料
	data01 := <-intChan
	data02 := <-intChan
	// 注意,在沒有使用協程的情況下,如果管道的資料已經全部取出,那麼再取就會報錯"fatal error: all goroutines are asleep - deadlock!"。
	// data03 := <-intChan

	fmt.Printf("data = %d\n", data01)
	fmt.Printf("intChan的實際大小: %d, 容量是: %d\n", len(intChan), cap(intChan))

	fmt.Printf("data = %d\n", data02)
	// fmt.Printf("data = %d\n", data03)
}

2.3 無緩衝管道

package main

import (
	"fmt"
	"sync"
	"time"
)

var wg sync.WaitGroup

func Consumer(ch chan bool) {
	
	// 在沒有拿到資料之前,下面這行程式碼會一直處於阻塞狀態喲!
	data := <-ch

	fmt.Println("in Consumer ... ", data)

	wg.Done()
}

func main() {

	// 定義無緩衝管道
	ch := make(chan bool)

	wg.Add(1)

	go Consumer(ch)

	fmt.Printf("已經開啟Consumer協程...ch的容量為:%d,長度為:%d\n", cap(ch), len(ch))

	// 故意阻塞住主執行緒3秒。
	time.Sleep(time.Second * 3)

	// 阻塞一段時間後,主執行緒再嘗試傳送資料,只要主執行緒傳送資料,Consumer協程就可以不阻塞啦~
	ch <- true

	fmt.Printf("main函式執行結束...ch的容量為:%d,長度為:%d\n", cap(ch), len(ch))
	wg.Wait()
}

3.管道的關閉

3.1 管道關閉操作結果概述

上面的表格中總結了對不同狀態下的通道執行相應操作的結果。

一個通道值是可以被垃圾回收掉的。通道通常由傳送方執行關閉操作,並且只有在接收方明確等待通道關閉的訊號時才需要執行關閉操作。

它和關閉檔案不一樣,通常在結束操作之後關閉檔案是必須要做的,但關閉通道不是必須的。
		
關閉後的通道有以下特點:
	- 對一個關閉的通道再傳送值就會導致 panic。
	- 對一個關閉的通道進行接收會一直獲取值直到通道為空。
	- 對一個關閉的並且沒有值的通道執行接收操作會得到對應型別的零值。
	- 關閉一個已經關閉的通道會導致panic。

3.2 管道關閉案例

package main

import "fmt"

func main() {

	var strChan chan string

	strChan = make(chan string, 5)

	strChan <- "JasonYin"
	strChan <- "https://www.cnblogs.com/yinzhengjie"
		
	// 我們透過呼叫內建的close函式來關閉通道。
	close(strChan)
	// 管道不能重複關閉,否則會報錯"panic: close of closed channel"
	// close(strChan)

	// 關閉管道後就不能寫入資料了,會報錯"panic: send on closed channel"
	// strChan <- "尹正傑"

	// 管道關閉後,是可以讀取資料的
	data := <-strChan

	fmt.Printf("data = %v\n", data)

}

3.3 判斷通道是否關閉

package main

import (
	"fmt"
)

func Consumer(ch chan string) {
	for {
		/*
			對一個通道執行接收操作時支援使用如下多返回值模式。
				- value:
					從通道中取出的值,如果通道被關閉則返回對應型別的零值。
				- ok:
					通道ch關閉時返回 false,否則返回true。
		*/
		value, ok := <-ch
		if !ok {
			fmt.Println("通道已關閉")
			break
		}
		fmt.Printf("value: %#v ok: %#v\n", value, ok)
	}
}

func main() {
	ch := make(chan string, 2)
	ch <- "尹正傑"
	ch <- "https://www.cnblogs.com/yinzhengjie"

	close(ch)

	Consumer(ch)
}

4.管道的遍歷

package main

import "fmt"

func listChannel(ch chan bool) {

	/*
	   管道的遍歷:
	   	管道支援for-range的方式進行遍歷。

	   管道遍歷注意三個細節:
	   - 1.如果管道沒有關閉,則會出現deadlock的錯誤;
	   - 2.如果管道已經關閉,則會正常遍歷資料,遍歷完後,就會退出遍歷;
	   - 3.目前Go語言中並沒有提供一個不對通道進行讀取操作就能判斷通道是否被關閉的方法,不能簡單的透過len(ch)操作來判斷通道是否被關閉;
	*/
	for value := range ch {
		fmt.Printf("value = %t\n", value)
	}
}

func main() {

	var boolChan chan bool

	boolChan = make(chan bool, 6)

	for i := 0; i < 2; i++ {
		boolChan <- true
		boolChan <- false
		boolChan <- true
	}

	// 關閉管道,避免在遍歷時出錯
	close(boolChan)

	listChannel(boolChan)

}

5.協程和管道協同工作案例

package main

import (
	"fmt"
	"sync"
	"time"
)

var wg sync.WaitGroup

// Producer 生產者產生資料
func Producer(strChan chan string) {
	defer wg.Done()

	for i := 1; i <= 20; i++ {
		data := fmt.Sprintf("餃子id = %d", i)

		strChan <- data

		fmt.Printf("Duang~新包的%s\n", data)

		time.Sleep(time.Second)
	}

	// 管道關閉,避免遍歷時報錯
	close(strChan)

}

// Consumer 消費者消費資料
func Consumer(strChan chan string) {
	defer wg.Done()

	// 遍歷資料
	for value := range strChan {
		fmt.Printf("吧唧,吃了%s\n", value)
	}

}

func main() {
	strChan := make(chan string, 20)

	wg.Add(2)

	// 開啟讀和寫的協程
	go Producer(strChan)
	go Consumer(strChan)

	wg.Wait()

	fmt.Println("吃餃子程式執行結束...")

}

6.宣告只讀只寫的管道

6.1 單向通道概述

在某些場景下我們可能會將管道作為引數在多個任務函式間進行傳遞。

我們會選擇在不同的任務函式中對管道的使用進行限制,比如限制管道在某個函式中只能執行傳送或只能執行接收操作。

6.2 宣告單向管道

package main

import (
	"fmt"
)

func main() {

	// 1.預設情況下,管道是雙向的,即可讀可寫
	var intChan01 chan int
	intChan01 = make(chan int, 3) // 初始化資料
	intChan01 <- 100              // 寫入資料到管道
	data01 := <-intChan01         // 從管道中讀取資料

	fmt.Printf("data01 = %d\n", data01)

	// 2.宣告單向的只寫管道,即只能寫不能讀
	var intChan02 chan<- int
	intChan02 = make(chan int, 3) // 初始化資料
	intChan02 <- 200              // 寫入資料到管道
	// data02 := <-intChan02         // 無法從管道中讀取資料,會報錯"invalid operation: cannot receive from send-only channel intChan02 (variable of type chan<- int)"
	// fmt.Printf("data02 = %d\n", data02)

	// 3.宣告單向的只讀管道,即只能讀不能寫
	var intChan03 <-chan int

	// intChan03 <- 200 // 無法寫入資料到管道,會報錯"invalid operation: cannot send to receive-only channel intChan03 (variable of type <-chan int)"

	if intChan03 != nil {
		data03 := <-intChan03
		fmt.Printf("data03 = %d\n", data03)
	}
}

6.3 單向管道作為引數傳遞

package main

import (
	"fmt"
)

// Producer 返回一個只寫的管道,並持續將符合條件的資料傳送至返回的管道中,資料傳送完成後會將返回的管道關閉
func Producer() <-chan int {
	ch := make(chan int, 2)

	// 建立一個新的goroutine執行傳送資料的任務
	go func() {
		// 將10以內的偶數返回
		for i := 0; i <= 10; i++ {
			if i%2 == 0 {
				ch <- i
			}
		}

		// 任務完成後關閉通道,避免另外的協程在遍歷時出錯。
		close(ch)
	}()

	return ch
}

// Consumer引數為只讀管道
func Consumer(ch <-chan int) int {
	sum := 0
	for value := range ch {
		fmt.Printf("in Consumer ... %d + %d \n", sum, value)
		sum += value
	}
	return sum
}

func main() {
	channel := Producer()

	result := Consumer(channel)

	fmt.Printf("in main ... \tresult = %d\n", result)
}

7.管道的阻塞

7.1 什麼時候會出現管道的阻塞

- 1.只讀不寫的情況下會阻塞管道

- 2.只讀不寫的情況下會阻塞管道

7.2 只寫不讀緩衝管道滿就會阻塞

package main

import (
	"fmt"
	"sync"
	"time"
)

var wg sync.WaitGroup

// Producer 生產者產生資料
func Producer(strChan chan string) {
	defer wg.Done()

	for i := 1; i <= 20; i++ {
		data := fmt.Sprintf("餃子id = %d", i)

		strChan <- data

		fmt.Printf("Duang~新包的%s\n", data)

		time.Sleep(time.Second)
	}

	// 管道關閉,避免遍歷時報錯
	close(strChan)

}

// Consumer 消費者消費資料
func Consumer(strChan chan string) {
	defer wg.Done()

	// 遍歷資料
	for value := range strChan {
		fmt.Printf("吧唧,吃了%s\n", value)
	}

}

func main() {
	// 緩衝管道大小為5
	strChan := make(chan string, 5)

	wg.Add(1)

	// 只寫不讀緩衝管道滿就會阻塞,報錯: "fatal error: all goroutines are asleep - deadlock!"
	go Producer(strChan)
	// go Consumer(strChan)

	wg.Wait()

	fmt.Println("吃餃子程式執行結束...")

}

7.3 只讀不寫的情況下會阻塞管道

package main

import (
	"fmt"
	"sync"
	"time"
)

var wg sync.WaitGroup

// Producer 生產者產生資料
func Producer(strChan chan string) {
	defer wg.Done()

	for i := 1; i <= 20; i++ {
		data := fmt.Sprintf("餃子id = %d", i)

		strChan <- data

		fmt.Printf("Duang~新包的%s\n", data)

		time.Sleep(time.Second)
	}

	// 管道關閉,避免遍歷時報錯
	close(strChan)

}

// Consumer 消費者消費資料
func Consumer(strChan chan string) {
	defer wg.Done()

	// 遍歷資料
	for value := range strChan {
		fmt.Printf("吧唧,吃了%s\n", value)
	}

}

func main() {
	// 緩衝管道大小為5
	strChan := make(chan string, 5)

	wg.Add(1)

	// 只讀不寫緩衝管道就會阻塞,報錯: "fatal error: all goroutines are asleep - deadlock!"
	// go Producer(strChan)
	go Consumer(strChan)

	wg.Wait()

	fmt.Println("吃餃子程式執行結束...")

}

二.select多路複用

1.select語句概述

在某些場景下我們可能需要同時從多個管道接收資料。 

Go語言內建了select關鍵字,解決多個管道的選擇問題,也可以叫做多路複用,可以從多個管道中隨機公平地選擇一個來執行。

select語句具有以下特點:
	- 1.case後面必須進行IO操作,不能是等值,隨機去選擇一個IO操作;
	- 2.default防止select被阻塞住,如果沒有case符合,則走default分支;

2.select案例

package main

import (
	"fmt"
	"time"
)

func ProducerInt(ch chan int) {

	ch <- 10

	for i := 100; i <= 110; i++ {
		time.Sleep(time.Second * 3)
		ch <- i
	}
}

func ProducerStr(ch chan string) {

	for i := 0; i < 10; i++ {
		time.Sleep(time.Second * 2)
		ch <- fmt.Sprintf("golang00%d\n", i)
	}
}

func main() {

	// 定義一個int管道
	intChan := make(chan int, 1)

	// 定義一個string管道
	strChan := make(chan string, 1)

	go ProducerInt(intChan)
	go ProducerStr(strChan)

	for count := 1; count <= 60; count++ {
		// select和前面學習的switch語句執行邏輯很相似
		select {
		// case後面必須進行IO操作,不能是等值,隨機去選擇一個IO操作,多個case如果都符合,則會隨機選擇一個去執行。
		case value := <-intChan:
			fmt.Printf("intChan = %v\n", value)
		case value := <-intChan:
			fmt.Printf("strChan = %v\n", value)
			// 如果所有的case語句都不符合,則走預設的default語句喲~
		default:
			fmt.Printf("程式已經執行%d秒\n", count)
			time.Sleep(time.Second * 1)
		}
	}

}

相關文章