golang使用通道時需要注意的一些問題
環(huán)境
- Go 1.20
- Windows 11
常識
1.定義通道變量:
ch := make(chan int) // 可存放int類型數(shù)據(jù),緩沖為0 ch := make(chan any) // 可存放任意類型數(shù)據(jù),緩沖為0 ch := make(chan int, 5) // 存放int類型數(shù)據(jù),緩沖為5 // 默認的通道是既可以寫入又可以讀取的,但我們也可以限制通道的方向 ch := make(<-chan int) // 只能從此通道讀取數(shù)據(jù),且不能關(guān)閉此通道 ch := make(chan<- int) // 只能寫入數(shù)據(jù)到此通道 length := len(ch) // 通道里有多少個數(shù)據(jù) capacity := cap(ch) // 通道的緩沖區(qū)大小
2.通道遵循FIFO先入先出規(guī)則,可以保證元素的順序
3.通道是并發(fā)安全的,不會因多個協(xié)程的同時寫入而發(fā)生數(shù)據(jù)錯亂
注意點
下面的代碼例子會經(jīng)常出現(xiàn)調(diào)用display函數(shù),這是我自己定義的一個函數(shù),主要用于打印信息,代碼如下:
func display(msg ...any) { fmt.Print(time.Now().Format(time.DateTime), " ") fmt.Println(msg...) }
為了減少代碼冗余,下面的代碼例子就不再貼出此函數(shù)的代碼了。
1、對一個沒有關(guān)閉的通道進行讀寫時,如果遇上了阻塞,并且此時已經(jīng)沒有其它活躍(非阻塞)的協(xié)程在運行了,會報deadlock錯誤!
怎么理解這句話呢,首先要了解讀寫通道時什么情況下會阻塞:
- 往緩沖已滿的通道寫入數(shù)據(jù)時會阻塞
- 讀取空的通道會阻塞
- 通道未初始化,例如var ch chan int就是未初始化的
針對第1點,假設(shè)通道緩沖是N,那么在第 N + 1 次寫入時會阻塞(定義通道變量時如果不指定N的大小,則N默認等于0)
針對第2點,如果這個空的通道是已關(guān)閉的,則不會阻塞,讀取到的是這個通道數(shù)據(jù)類型的零值
例子1:
func main() { ?? ?ch := make(chan int) ?? ?// 協(xié)程1 ?? ?go func() { ?? ??? ?for i := 0; i < 3; i++ { ?? ??? ??? ?display("準備發(fā)送:", i) ?? ??? ??? ?ch <- i ?? ??? ??? ?display("已發(fā)送完畢:", i) ?? ??? ?} ?? ?}() ?? ?for data := range ch { ?? ??? ?display("獲得數(shù)據(jù):", data) ?? ?} }
上面代碼運行后會報錯:fatal error: all goroutines are asleep - deadlock!
原因是,當【協(xié)程1】往通道寫入3個數(shù)據(jù)后,【協(xié)程1】就結(jié)束運行了,這時【main協(xié)程】(是的,main函數(shù)也是運行在協(xié)程里的)讀取出這3個數(shù)據(jù)后,并沒有退出for-range循環(huán),而是繼續(xù)讀取已空的ch通道,發(fā)生了阻塞,但這時只有【main協(xié)程】在運行了,只剩下一個協(xié)程,所以報錯。
例子1修改一下:
func main() { ?? ?ch := make(chan int) ?? ?// 協(xié)程1 ?? ?go func() { ?? ??? ?for i := 0; i < 3; i++ { ?? ??? ??? ?display("準備發(fā)送:", i) ?? ??? ??? ?ch <- i ?? ??? ??? ?display("已發(fā)送完畢:", i) ?? ??? ?} ?? ?}() ? ? // 協(xié)程2 ?? ?go func() { ?? ??? ?for data := range ch { ?? ??? ??? ?display("獲得數(shù)據(jù):", data) ?? ??? ?} ?? ?}() ? ? // 死循環(huán) ?? ?for { ?? ?} }
經(jīng)修改后代碼不會再報錯了,原因是,【協(xié)程1】退出后,雖然【協(xié)程2】還在阻塞式地讀取空通道,但這時除了【協(xié)程2】以外,還有一個活躍的【main協(xié)程】在運行,所以不會報錯。
例子1再修改下:
func main() { ?? ?ch := make(chan int) ?? ?// 協(xié)程1 ?? ?go func() { ?? ??? ?for i := 0; i < 3; i++ { ?? ??? ??? ?display("準備發(fā)送:", i) ?? ??? ??? ?ch <- i ?? ??? ??? ?display("已發(fā)送完畢:", i) ?? ??? ?} ?? ??? ?close(ch) // 新添加代碼 ?? ?}() ?? ?for data := range ch { ?? ??? ?display("獲得數(shù)據(jù):", data) ?? ?} }
協(xié)程1在寫入完所有數(shù)據(jù)后,使用close(ch)關(guān)閉了通道,這時也不會再報錯了。原因是,對于已關(guān)閉的通道,for-range循環(huán)讀取完通道的數(shù)據(jù)后,會自動結(jié)束循環(huán),不會阻塞在讀取通道處,所以不會報錯。
2、給一個已關(guān)閉的通道發(fā)送數(shù)據(jù),或者再次關(guān)閉一個已關(guān)閉的通道,會導致panic
這句話告訴我們,當發(fā)送方不再需要發(fā)送數(shù)據(jù)時,可以關(guān)閉通道,但不能讓接收方去關(guān)閉。
因為接收方并不知道發(fā)送方是否還需要發(fā)送數(shù)據(jù),如果胡亂關(guān)閉了通道,會導致發(fā)送方觸發(fā)panic
3、已關(guān)閉的通道是可以繼續(xù)讀取里面的數(shù)據(jù)的
func main() { ?? ?ch := make(chan int, 2) ?? ?ch <- 123 ?? ?ch <- 456 ?? ?close(ch) ?? ?// 使用for-range讀取已關(guān)閉通道,通道空了之后會自動跳出循環(huán) ?? ?for data := range ch { ?? ??? ?display(data) ?? ?} ?? ?// 方式2:使用ok變量判斷通道是否已空 ?? ?/*for { ?? ??? ?data, ok := <-ch ?? ??? ?if !ok { ?? ??? ??? ?break ?? ??? ?} ?? ??? ?display(data) ?? ?}*/ ? ? // 方式3:通過通道長度來判斷通道是否已空 ?? ?/*num := len(ch) ?? ?for i := 0; i < num; i++ { ?? ??? ?data := <-ch ?? ??? ?display(data) ?? ?}*/ }
4、雙向通道可以傳遞給參數(shù)為單向通道的函數(shù)
// 函數(shù)參數(shù)是單向通道 func sendMessage(in chan<- int) { ?? ?for i := 0; i < 3; i++ { ?? ??? ?in <- i ?? ?} ?? ?close(in) } func main() { ?? ?ch := make(chan int) // 雙向通道 ?? ?go sendMessage(ch) ?? ?for data := range ch { ?? ??? ?display(data) ?? ?} }
5、當讀取通道與select搭配使用,并且設(shè)置了超時時間時,通道一定要設(shè)置緩沖
先看例子:
func sendMessage(in chan<- int, sleep time.Duration) { ?? ?time.Sleep(sleep) ?? ?in <- 1 } func main() { ?? ?display("開始") ?? ?display("協(xié)程數(shù)量:", runtime.NumGoroutine()) ?? ?ch1 := make(chan int) // 錯誤 ?? ?// 正確:ch1 := make(chan int, 1) ? ? // 協(xié)程1 ?? ?go sendMessage(ch1, 5 * time.Second) ?? ?select { ?? ?case v := <-ch1: ?? ??? ?display("從通道1獲取到了數(shù)據(jù):", v) ?? ?case <-time.After(1 * time.Second): ?? ??? ?display("超時了,退出select") ?? ?} ?? ?for { ?? ??? ?display("協(xié)程數(shù)量:", runtime.NumGoroutine()) ?? ??? ?time.Sleep(1 * time.Second) ?? ?} }
如上面代碼所示,一開始我們創(chuàng)建了一個無緩沖的通道ch1,然后開啟【協(xié)程1】,【協(xié)程1】在 5 秒后會往通道寫入一個數(shù)據(jù),但select的超時時間只設(shè)置了 1 秒。也就是說,在【協(xié)程1】往通道寫入數(shù)據(jù)前,select語句就已經(jīng)因為超時而結(jié)束了,此時的ch1通道已經(jīng)沒有接收方,只剩下發(fā)送方了。往一個無緩沖的通道寫入數(shù)據(jù)會導致【協(xié)程1】阻塞,而且沒有了接收方,【協(xié)程1】就會永遠阻塞下去,無法結(jié)束退出,從而導致協(xié)程泄露。
觀察超時后打印出來的協(xié)程數(shù)量,一直都是2,不會降低為1,也證實了上面的說法。所以在定義通道變量時,一定要設(shè)置緩沖區(qū)。
其實調(diào)高 select的超時時間,也能解決這個問題。但有時候我們可能無法得知協(xié)程具體的執(zhí)行耗時,從而預(yù)估出一個合理的超時時間,所以穩(wěn)妥起見,還是定義一個帶緩沖的通道比較好。
到此這篇關(guān)于golang使用通道時需要注意的一些問題的文章就介紹到這了,更多相關(guān)golang 通道內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
詳解Go語言中自定義結(jié)構(gòu)體能作為map的key嗎
在Go中,引用類型具有動態(tài)的特性,可能會被修改或指向新的數(shù)據(jù),這就引發(fā)了一個問題—能否將包含引用類型的自定義結(jié)構(gòu)體作為map的鍵呢,本文就來和大家想想講講2023-06-06Go語言fsnotify接口實現(xiàn)監(jiān)測文件修改
這篇文章主要為大家介紹了Go語言fsnotify接口實現(xiàn)監(jiān)測文件修改的示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-06-06