Golang中channel的用法舉例詳解
前言
在golang并發(fā)編程實踐中,channel的正確運用直接影響程序的健壯性和執(zhí)行效率。本文將深入探討幾種提升channel使用效能的典型場景與實現(xiàn)策略。
1.channel的類別
主要有2種:
有緩存 Channel(buffered channel),使用 make(chan type, capacity int) 創(chuàng)建
無緩存 Channel(unbuffered channel),使用 make(chan type) 創(chuàng)建
其中 ,type為 Channel 傳遞數(shù)據(jù)的類型,capacity 為緩存大小。
unbuffered channel:阻塞、同步模式
- sender端向channel中send一個數(shù)據(jù),然后阻塞,直到receiver端將此數(shù)據(jù)receive
- receiver端一直阻塞,直到sender端向channel發(fā)送了一個數(shù)據(jù)
buffered channel:非阻塞、異步模式
- sender端可以向channel中send多個數(shù)據(jù)(只要channel容量未滿),容量滿之前不會阻塞
- receiver端按照隊列的方式(FIFO,先進先出)從buffered channel中按序receive其中數(shù)據(jù)
2.channel的狀態(tài)
主要有3種狀態(tài):
- actived,正常狀態(tài),可以正常的讀receive,寫send
- nil,未初始化狀態(tài),即只進行了聲明但還尚未分配內(nèi)存,或者是channel被主動賦值為了nil
- closed,關(guān)閉狀態(tài)。
注意:channel被close后,它的狀態(tài)并不是nil。因為nil channel是不能讀取的,會panic。但是close后的channel,如果管道里還有數(shù)據(jù),是可以通過range正常讀取出來的。
3.channel的操作
常用的操作有這4種:
- 讀,<- ch
- 寫,ch <-
- 關(guān)閉, close(ch)
- 遍歷,for v := range ch {}
4.組合操作
前面所說的3種狀態(tài),和4種操作,組合起來后的結(jié)果如下:
| 操作\狀態(tài) | actived | close | nil |
|---|---|---|---|
| <-ch (讀) | 成功或者阻塞 | 零值 | 死鎖 |
| ch<- (寫) | 成功或者阻塞 | panic | 死鎖 |
| close | 成功 | panic(重復關(guān)閉) | panic |
| for range | 成功 | 成功(break) | 死鎖 |
有2個特殊點需要說明:
4.1 for range closed_chan
場景1:channel已關(guān)閉且無剩余數(shù)據(jù)循環(huán)立即退出:如果channel在關(guān)閉時已經(jīng)沒有數(shù)據(jù),for range循環(huán)不會執(zhí)行任何迭代,直接終止。
ch := make(chan int)
close(ch)
for v := range ch { // 循環(huán)不執(zhí)行,直接退出
// 代碼不會執(zhí)行
}
場景2:channel已關(guān)閉但有剩余數(shù)據(jù)讀取所有數(shù)據(jù)后退出:如果channel關(guān)閉時仍有數(shù)據(jù)未被讀取,for range會讀取所有剩余數(shù)據(jù),然后正常退出循環(huán)。
ch := make(chan int, 2)
ch <- 1
ch <- 2
close(ch)
for v := range ch {
fmt.Println(v) // 輸出1、2
}
場景3:未關(guān)閉的Channel會導致阻塞如果channel未關(guān)閉且無數(shù)據(jù),for range會一直阻塞等待數(shù)據(jù),可能導致goroutine泄漏。
4.2 nil channel在select中的行為
如上面的表格所展示:對nil channel進行讀寫,都會導致死鎖(永久阻塞)。
無論是發(fā)送(ch <- v)還是接收(<- ch),操作nil channel的代碼會永久阻塞。
var ch chan int // ch是nil ch <- 1 // 永久阻塞(發(fā)送到nil channel) <-ch // 永久阻塞(從nil channel接收)
但是,如果是在select中,則select會忽略nil channel的case:
- 當select的某個case操作的是nil channel時,該case會被視為未就緒,直接跳過。
- 如果其他case中有就緒的channel操作,select會正常執(zhí)行這些case。
- 如果所有case都未就緒(包括nil channel的case),且沒有default分支,則select會阻塞等待,但不會觸發(fā)死鎖(除非整個goroutine再無其他代碼可執(zhí)行)。
場景1:nil channel與其他有效case共存
var ch chan int // ch是nil
timeout := time.After(1 * time.Second)
select {
case <-ch: // 該case被跳過(nil channel)
fmt.Println("Received from ch")
case <-timeout:
fmt.Println("Timeout") // 1秒后執(zhí)行
}
結(jié)果:select會忽略<-ch(nil channel),等待timeout就緒后執(zhí)行。
不會死鎖:因為存在其他有效case(timeout)。
場景2:所有case均為nil channel
var ch1, ch2 chan int // 均為nil
select {
case <-ch1: // 被跳過
case <-ch2: // 被跳過
}
結(jié)果:select會永久阻塞,但如果整個goroutine沒有其他代碼可執(zhí)行,會觸發(fā)死鎖(fatal error: all goroutines are asleep - deadlock!)。
關(guān)鍵點:死鎖是否發(fā)生取決于整個goroutine的狀態(tài),而不僅僅是select本身。
select的設(shè)計機制:
- select會動態(tài)檢查所有case的就緒狀態(tài),跳過未就緒的case(包括nil channel)。
- 只要存在其他就緒的case(如定時器、非nil channel等),select就能正常執(zhí)行。
5.channel常見用法
5.1 使用for range讀取channel
場景:需要持續(xù)不斷從channel讀取數(shù)據(jù)
說明:采用range關(guān)鍵字進行通道遍歷,當發(fā)送端關(guān)閉通道時,循環(huán)自動終止。這種方式避免了手動檢測通道狀態(tài)的繁瑣操作,同時保證不會讀取到無效零值。
示例:
for v := range ch{
doSomething(v)
}
5.2 使用_, ok判斷channel狀態(tài)
場景:雙返回值驗證機制,用于讀取channel但不確定channel是否已經(jīng)關(guān)閉
說明:當不確定通道是否關(guān)閉時,采用特殊語法進行安全校驗。返回值狀態(tài)指示符ok為true表示成功接收有效數(shù)據(jù),false則標志通道已關(guān)閉。
示例:
if v, ok := <-ch; ok {
doSomething(v)
}
5.3 使用select進行多路channel處理
場景:選擇性路由機制。select對多個channel同時處理時,會先處理最先發(fā)生的channel
說明:當需要同時監(jiān)聽多個數(shù)據(jù)源時,select語句可實現(xiàn)智能路由。注意nil channel的特殊處理,讀取會永久阻塞,寫入將導致運行時異常,但是注意4.2節(jié)中提到的select對于nil channel的特殊情況。
示例:
select {
case taskCh <- newTask:
processTask()
case <-shutdownSignal:
terminate()
}
5.4 使用channel控制讀寫權(quán)限
場景:協(xié)程對某個channel只讀或者只寫時
說明:通過聲明只讀/只寫類型channel,增強代碼可維護性。這種類型約束可防止意外的反向操作,降低運行時panic風險。
示例:
// 只寫操作
func writeOnly(n int) <-chan int {
ch := make(chan int)
go func() {
defer close(ch) //必須關(guān)閉channel以通知外面的其它工作協(xié)程退出.
//不然,在外面無法close一個單向的channel,導致死鎖
for i:=0; i<n; i++ {
ch <- i
}
}()
return out
}
// 只讀操作
func readOnly(in <-chan int) {
for v := range in {
doSomething(v)
}
}
5.5 使用channel進行并發(fā)控制
場景:同步,異步和并發(fā)調(diào)用
說明:有緩存chan(buffered channel)是異步的,可提供給多個協(xié)程同時處理,提高系統(tǒng)的并發(fā)性能。而無緩存chan(unbuffered channel)是同步的。
// 有緩存channel ch1 := make(chan int, 1) // 無緩存channel ch2 := make(chan int) ch3 := make(chan int, 0)
示例: 并發(fā)處理模型
func doWorker(inCh <-chan int, outCh chan<- int, wg *sync.WaitGroup) {
defer wg.Done()
for v := range inCh {
outCh <- v * 10
}
}
func concurrentProcess() {
inCh := writeOnly(100)
outCh := make(chan int, 10)
var wg sync.WaitGroup
// 同時運行5個協(xié)程,從inCh中并發(fā)讀取數(shù)據(jù),并發(fā)寫入outCh
for i := 0; i < 5; i++ {
wg.Add(1)
go doWorker(inCh, outCh, &wg)
}
//等待所有worker完成并關(guān)閉outCh
go func() {
wg.Wait()
close(outCh)
}()
for v := range outCh {
fmt.Println(v)
}
}
5.6 超時控制
場景:在一些需要進行超時控制的情況下
說明:通過結(jié)合定時器(select & time.After)實現(xiàn)操作時效控制,避免長期阻塞。特別注意定時channel的資源釋放問題。
示例:
func doWorker() <-chan int {
ch := make(chan int)
go func() {
// do something for ch
}()
return ch
}
func doWithTimeout(timeout time.Duration) (int, error) {
select {
case v := <-doWorker2():
return v, nil
case <-time.After(timeout):
return 0, fmt.Errorf("timeout")
}
}
func main() {
v, err := doWithTimeout(1 * time.Second)
fmt.Printf("v:%d, err:%v\n", v, err)
}
輸出: v:0, err:timeout
5.7 非阻塞讀寫channel
場景:對channel進行非阻塞式的讀或者寫
說明:通過default分支實現(xiàn)即時返回,適用于不可阻塞的實時系統(tǒng)。需要與帶緩沖通道配合使用。
示例:
非阻塞的立即返回方案
func unblockWrite(ch chan int, v int) error {
select {
case ch <- v:
return nil
default:
return fmt.Errorf("channel write blocked")
}
}
func unblockRead(ch chan int) (int, error) {
select {
case v := <-ch:
return v, nil
default:
return 0, fmt.Errorf("channel read blocked")
}
}
5.8 級聯(lián)close channel
場景:進行優(yōu)雅關(guān)閉,廣播通知所有協(xié)程退出
說明:關(guān)閉channel會產(chǎn)生廣播效應,所有接收此channel的協(xié)程都會收到零值。結(jié)合sync.WaitGroup可實現(xiàn)安全終止。
示例:
type Manager struct {
stopCh chan struct{}
workCh chan struct{}
wg sync.WaitGroup
}
func (m *Manager) Shutdown() {
close(m.stopCh)
//等待所有其它協(xié)程退出
m.wg.Wait()
}
func (m *Manager) workLoop() {
for {
select {
case v := <-m.workCh:
go doWorker(v)
case <-m.stopCh: //close后會讀取到零值
return
}
}
}
5.9 信號事件載體
場景:定義的channel,僅用來傳遞事件/信號,無需傳遞數(shù)據(jù)
說明:當僅需事件通知而不傳遞數(shù)據(jù)時,采用空結(jié)構(gòu)體通道可最小化內(nèi)存消耗。
示例:在5.8節(jié)中,stopCh就是用于事件傳遞的channel。它并不需要傳遞數(shù)據(jù),只需要向其它的所有協(xié)程發(fā)出終止信號。
5.10高效數(shù)據(jù)傳輸
場景:用于性能優(yōu)化,傳遞指針,而非拷貝數(shù)據(jù)
說明:對于大型數(shù)據(jù)結(jié)構(gòu),傳遞指針可顯著降低通道操作的復制開銷。需注意并發(fā)訪問時的數(shù)據(jù)競爭問題。
示例:
type Payload struct {
// 大數(shù)據(jù)結(jié)構(gòu)
}
payloadChan := make(chan *Payload, 10)
以上就是關(guān)于channel的一些實踐技巧,合理運用能有效提升并發(fā)程序的執(zhí)行效率和代碼可維護性。開發(fā)者應根據(jù)具體場景選擇適當?shù)哪J?,并注意資源管理與并發(fā)安全問題。
總結(jié)
到此這篇關(guān)于Golang中channel用法舉例詳解的文章就介紹到這了,更多相關(guān)Golang中channel用法內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Go語言k8s?kubernetes使用leader?election實現(xiàn)選舉
這篇文章主要為大家介紹了Go語言?k8s?kubernetes?使用leader?election選舉,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-10-10
Golang利用channel協(xié)調(diào)協(xié)程的方法詳解
go?當中的并發(fā)編程是通過goroutine來實現(xiàn)的,利用channel(管道)可以在協(xié)程之間傳遞數(shù)據(jù),所以本文就來講講Golang如何利用channel協(xié)調(diào)協(xié)程吧2023-05-05
golang中按照結(jié)構(gòu)體的某個字段排序?qū)嵗a
在任何編程語言中,關(guān)乎到數(shù)據(jù)的排序都會有對應的策略,下面這篇文章主要給大家介紹了關(guān)于golang中按照結(jié)構(gòu)體的某個字段排序的相關(guān)資料,需要的朋友可以參考下2022-05-05
golang替換無法顯示的特殊字符(\u0000,?\000,?^@)
這篇文章主要介紹了golang替換無法顯示的特殊字符,包括的字符有\(zhòng)u0000,?\000,?^@等,下文詳細資料,需要的小伙伴可以參考一下2022-04-04

