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