Go語言 | goroutine不只有基礎的用法,還有這些你不知道的操作

TechFlow2019發表於2020-09-07

今天是golang專題第15篇文章,我們來繼續聊聊channel的使用。

在我們的上篇文章當中我們簡單介紹了golang當中channel的使用方法,channel是golang當中一個非常重要的設計,可以理解為生產消費者模式當中的佇列。但channel和佇列不一樣的是,golang當中整合了一些其他的用法,使得我們的使用更加靈活,開發併發相關的功能更加簡單。

select機制

我們來思考一個問題,假設我們的資料來源有多個,也就是說我們可能會從多個入口獲取資料,但是我們並不知道這些資料來源當中哪個先把資料準備好。我們希望實現輪詢這些channel,哪個資料準備好了就讀取哪個,否則就阻塞等待,這個功能應該怎麼辦呢?

我們當然可以自己用迴圈來實現,但是這顯然是不合理的,golang當中針對這個問題提供了專門的解決方法,它就是select關鍵字。

select機制並不是golang這門語言獨創的,早在Unix時代就有了select機制。通過select函式監控一系列檔案的控制程式碼,一旦其中一個發生了改動,select函式就會返回。而golang當中的select則用來在channel當中進行選擇,有點像是switch,寫出來的程式碼大概是這個樣子:

select {
    case <- chan1:
    case chan2 <- 1:
    default:
}

select後面跟多個case以及default,其中default並不是必須的case後面必須要接一個chan的操作,可以是從一個chan當中讀入資料,也可以是向一個chan當中寫入資料。如果所有的case都不成功,則會進入default語句當中,如果沒有default語句,那麼select會陷入阻塞。

一般情況下我們使用select是為了從多個資料來源中獲取資料,當多個chan同時有資料的時候,使用select可以讓我們避免判斷哪個資料來源資料ready的問題。

range機制

我們之前在介紹slice遍歷的時候曾經介紹過range機制,我們可以通過range來遍歷一個陣列或者是map。就像是這樣:

arr := make([]int0)
for i := range arr {
    // do something
}

mp := make(map[string]int)

for k, v := range mp {
    // do something
}

很多時候我們會把這個用法當做是迭代器的迭代,就像是Java和Python中的那樣。但實際上range機制的底層原理是chan,當我們使用range的時候,它表示會不斷地從chan當中接受值,直到它關閉。

所以我們也可以這樣遍歷一個chan當中的資料:

ch := make(chan int)

for c := range ch {
    // do something
}

超時機制

有沒有想過一個問題,channel的寫入和寫出都是阻塞的,也就是說如果是從chan當中讀取資料,必須要上游已經傳輸了才可以讀取到。同樣,如果往沒有緩衝區的chan寫入資料也需要下游消費了才能寫入成功。阻塞往往是有很大隱患的,如果處理不好很容易導致整個程式鎖死。

我們需要設計機制來解決這個問題,比較好的方案就是設定定時器,如果超過一定的時間chan還沒有響應成功的話,那麼就人工停止程式。這一點說起來還有點麻煩的,比如我們要啟動一個定時器,要手動終止goroutine,但是結合select機制其實並不難實現,我們來看程式碼。

timeout := make(chan bool)
go func() {
    time.Sleep(1e9)
    timeout <- true
}()


select {
 case <- ch:
     // do something
 case <- timeout: 
     // break
}

說白了很簡單,也就是我們額外啟動一個goroutine做休眠操作,當休眠結束之後也通過chan傳送訊息,這樣如果我們select先接受到了timeout的訊號就說明了程式已經超時了。當然這只是一個很簡單的demo,實際使用的話需要考慮的情況可能還會更多。

channel傳遞

有沒有想過一個問題,既然chan可以傳輸任何型別的資料,那麼我們能不能用一個chan傳輸一個chan呢?

這樣的操作是可以的,因為在有些場景當中相比於直接把資料傳輸給下游,我們傳輸讀取資料的chan可能更加方便。有點授人以漁的意思,更加厲害的是我們可以結合函數語言程式設計,把處理資料的函式一併傳輸給下游。這樣下游讀取到資料,並且用讀取到的處理函式來處理,這樣可以更加定製化,如果以後資料和處理方式都發生改動,也只需要在上游修改,可以更加解耦。

我們同樣來看一個demo:

type MetaData struct {
    value interface{}
    handler func(interface{}) int
    downstream chan interface
{}
}


func handle(queue chan *MetaData) {
    for data := range queue {
        data.downstream <- data.handler(data.value)
    }
}

這只是一個簡單的案例,想要在實際應用當中真的使用上還需要定義大量的介面以及做很多設計。我們只需要知道有這麼一種設計模式和用法就可以了。

單向channel

最後,我們來說說單向channel,也就是說我們指定channel是隻讀的或者是隻寫的。但其實這是一個偽命題,原因也很簡單,如果只寫資料沒人讀,或者是隻讀但是不能寫,那麼這個channel有什麼用呢?只有有人讀有人寫才可以完成資料流通不是嗎?

的確如此,所以這裡所說的單向channel其實並不是真正意義上的單向,只是說我們為了規範,對使用方進行限制。比如說我們限定在消費函式當中不能寫入,在生產函式當中不能消費。我們在通過函式傳遞chan的時候,可以通過加上限定讓chan在函式當中變成單向的。

var ch chan <- float32 // 只寫chan
var ch <- chan float32 // 只讀chan

除此以外我們還可以把一個正常的chan轉化成單向的chan:

var ch chan int
ch1 := <- chan int(ch)
ch2 := chan <- int(ch)

我們一般不在程式當中做這樣的轉化,而是用在函式引數當中,這也主要是為了起到規範的作用。

func Test(ch <- chan int) {
    for val := range ch {
        // do something
    }
}

今天的文章到這裡就結束了,如果喜歡本文的話,請來一波素質三連,給我一點支援吧(關注、轉發、點贊)。

- END -

原文連結,求個關注

{{uploading-image-498941.png(uploading...)}}

相關文章