channel
為什么需要channel
使用全局變量加鎖同步來解決goroutine的競爭,可以但不完美
-
難以精確控制等待時間?(主線程無法準確知道所有 goroutine 何時完成)。
-
全局變量容易引發競態條件?(即使加鎖,代碼復雜度也會增加)。
-
不夠優雅,Go 更推薦使用 ?channel? 進行通信。
channel基本介紹
- ?channel(通道)?? 是一種用于 ?goroutine(協程)之間通信和同步? 的機制。
- channel的本質是一個隊列,遵循先進先出(FIFO)
- channel是有類型的,一種channel只能存儲類型與該channel的類型相同的數據
- channel是線程安全的,不需要加鎖
- channel是引用類型,必須初始化make后才能寫入數據,未初始化的channel是nil
channel快速入門
channel的聲明
var 變量名 chan 數據類型
channel的初始化
var myChan chan int = make(chan int, 2)
發送數據到channel
myChan <- 10
myChan <- 20
此處注意:channel不像map等會自動擴容,channel接收的數據數的最大值在make函數里已經自定義完成了,容量就是一直這么大,不會改變。
從channel接收數據
num := <-myChan
fmt.Println(num)
輸出結果:10
此處接收數據不能超出myChan現有的數據量,也就是myChan的長度。
channel的細節
-
channel中只能存放指定的數據類型
-
channle的數據放滿后,就不能再放入了
-
channel放滿后,如果從channel取出數據后,可以繼續放入
<-myChan也是取出了數據,只是沒有被接收罷了
- 在沒有使用協程的情況下,如果channel數據取完了,再取,就會報deadlock
易錯點
如果想在channel中輸入多樣的數據類型,就將channel聲明成空接口interface{}的類型
代碼示例:
func main() {
myChan := make(chan interface{}, 3)
myChan <- 10
myChan <- “sa”
person := Person{“xxx”}
myChan <- person
<-myChan
<-myChan
person2 := <-myChan
// fmt.Printf(“person2的type%T,值%v,name%v”, person2, person2, person2.Name)
person3 := person2.(Person)
fmt.Printf(“person3的type:%T,值:%v,name:%v”, person3, person3, person3.Name)
}
輸出結果:person3的typemain.Person,值{xxx},namexxx
唉,為什么要person3 := person2.(Person)呢,直接用fmt.Printf(“person2的type%T,值%v,name%v”, person2, person2, person2.Name)不好嗎?
當然不好啦,使用這個代碼會報錯,會說person2.Name undefined
為什么呢?
因為person2是從channel中讀取的interface{}類型,雖然實際值是Person類型,但編譯器不知道其具體類型,因此無法直接訪問Name字段。
所以要通過person3 := person2.(Person),提取一個類型斷言后的值,將其轉換為具體的Person類型,然后才能訪問其字段
channel的關閉
發送方可以關閉 channel,表示不再發送數據
內置函數:close(ch)
??關閉后,仍然可以接收數據?(直到 channel 為空)。
??向已關閉的 channel 發送數據會 panic。
channel的遍歷
- 通過 for-range 遍歷
代碼示例:
func main() {
myChan := make(chan int, 3)
myChan <- 10
myChan <- 30
myChan <- 20
close(myChan)
for v := range myChan {
fmt.Printf(“%v\n”, v)
}
}
輸出結果:
10
30
20
for range會一直從 ch接收數據,直到 ch被關閉。
如果 ch未關閉,for range會一直阻塞,可能導致死鎖。
手動檢查 channel 是否關閉
可以用 value, ok := <-ch的方式檢查 channel 是否關閉, 如果 channel 關閉,ok 為 false
- 傳統for循環
func main() {
myChan := make(chan int, 3)
myChan <- 10
myChan <- 30
myChan <- 20
len := len(myChan)
for i := 0; i < len; i++ {
fmt.Println(<-myChan)
}
}
也可以正常有序輸出,輸出結果與for-range一致
channel的阻塞
阻塞是指 goroutine 在 channel 操作上等待,但不會導致整個程序卡死。?
- 從空的 channel 接收數據
- 向已滿的緩沖 channel 發送數據
- 讀比寫的操作慢,導致出現(2)情況
- 寫比讀的操作慢,導致出現(1)情況
channel的死鎖
死鎖是指所有 goroutine 都在等待對方釋放資源,導致程序無法繼續執行。
- 所有 goroutine 都在等待 channel
- 未關閉 channel 導致 for range死鎖
使用細節
- channel可以聲明為只讀,或者只寫性質
此處只讀只寫只是一種屬性,并不會改變channel的類型,該是chan int 就還是chan int
chan<- int 是只寫
<-chan int 是只讀
代碼示例:
package main
import (
“fmt”
“math/rand”
“time”
)
// 只寫通道:用于發送訂單
func orderProducer(orderChan chan<- int, doneChan chan<- struct{}) {
defer close(orderChan) // 生產結束后關閉訂單通道
for i := 1; i <= 5; i++ {orderID := rand.Intn(1000) + 1000 // 模擬生成訂單號fmt.Printf("📦 生成訂單 #%d (ID: %d)\n", i, orderID)orderChan <- orderIDtime.Sleep(time.Second) // 模擬生產間隔
}doneChan <- struct{}{} // 發送完成信號
}
// 只讀通道:用于處理訂單
func orderProcessor(orderChan <-chan int, doneChan chan<- struct{}) {
for orderID := range orderChan { // 自動檢測通道關閉
processTime := time.Duration(rand.Intn(1500)) * time.Millisecond
fmt.Printf(“處理訂單 ID: %d (耗時: %v)\n”, orderID, processTime)
time.Sleep(processTime)
}
doneChan <- struct{}{} // 發送完成信號
}
func main() {
// 初始化通道(帶緩沖)
orderChan := make(chan int, 3) // 訂單通道(緩沖3個訂單)
doneChan := make(chan struct{}, 2) // 控制通道(緩沖2個信號)
// 啟動服務
go orderProducer(orderChan, doneChan) // 訂單生產(只寫)
go orderProcessor(orderChan, doneChan) // 訂單處理(只讀)// 等待兩個服務完成
for i := 0; i < 2; i++ {<-doneChan
}
fmt.Println("所有訂單處理完成")
}
這段代碼中,main函數中定義的orderChan是一個chan int 類型,但他可以同時被使用在只讀和只寫的函數里,這就很大程度上的便于代碼的管理,防止誤操作。
- select解決 channel 阻塞問題
日常中,難以準確判斷讀取/寫入與關閉時機難以掌握,所以提出select,雖然select還是無法關閉channel,但是能防止防止讀取/寫入時的無限等待
代碼示例
for{
select {
case msg := <-ch1:
fmt.Println(“收到 ch1:”, msg)
case msg := <-ch2:
fmt.Println(“收到 ch2:”, msg)
case <-time.After(3 * time.Second): // 超時控制
fmt.Println(“讀取超時”)
return
}
}
如果多個 case 的 channel 同時就緒(例如多個 channel 都有數據可讀),select?會隨機選擇一個執行?(公平調度,避免饑餓問題)
select?自動忽略未就緒的 channel?(無論是否關閉),無需手動處理。
此處的ch1哪怕沒有關閉,也不會報錯,而是在無法從ch1取到值后,會暫時將這個case不考慮在執行case內
?每次執行 select時都會重新檢查所有 case的就緒狀態
還有,最后的return不能使用break代替,因為break只能退出select不能退出for循環,所以相當于重新開始了
return其實還可以用之前提到的label來代替,就是給這個for循環一個標簽,然后break label就好了 (但是這種方式并不建議,可讀性較差)
- recover來防止出現因為一個線程的錯誤導致其它線程無法進行
原錯誤代碼:
package main
import (
“fmt”
“time”
)
// 1. 循環打印 “hello,world”
func sayHello() {
for i := 0; i < 10; i++ {
fmt.Println(“hello,world”)
time.Sleep(1 * time.Second)
}
}
// 2. 測試未初始化的 map(會觸發 panic)
func test() {
var myMap map[int]string
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
}()
myMap[0] = “golang” // error: 未初始化的 map 賦值會導致 panic
}
// 3. 主函數(并發執行)
func main() {
go sayHello() // 啟動協程
go test() // 啟動協程(會崩潰)
// 主線程繼續執行
for i := 0; i < 10; i++ {fmt.Printf("main() ok=%d\n", i)time.Sleep(1 * time.Second)
}
}
輸出結果:
main() ok=0
hello,world
panic: assignment to entry in nil map
報了panic錯誤,主線程并沒有正常運行
修改代碼:
func test() {
var myMap map[int]string
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
}()
myMap[0] = “golang” // error: 未初始化的 map 賦值會導致 panic
}
將錯誤的test函數加上錯誤捕獲,異常處理
輸出結果:
hello,world
assignment to entry in nil map
main() ok=0
main() ok=1
hello,world
hello,world
main() ok=2
hello,world
main() ok=3
hello,world
main() ok=4
hello,world
main() ok=5
main() ok=6
hello,world
hello,world
main() ok=7
main() ok=8
hello,world
hello,world
main() ok=9
即使仍是錯誤,也依舊不影響其它線程