GO Channel詳解

Channel是Go中的一個(gè)核心類型稚瘾,你可以把它看成一個(gè)管道,通過它并發(fā)核心單元就可以發(fā)送或者接收數(shù)據(jù)進(jìn)行通訊(communication)。

它的操作符是箭頭 <- 翰苫。

ch <- v    // 發(fā)送值v到Channel ch中
v := <-ch  // 從Channel ch中接收數(shù)據(jù),并將數(shù)據(jù)賦值給v

(箭頭的指向就是數(shù)據(jù)的流向)

就像 map 和 slice 數(shù)據(jù)類型一樣, channel必須先創(chuàng)建再使用:

ch := make(chan int)

Channel 類型


Channel類型的定義格式如下:

ChannelType = ( "chan" | "chan" "<-" | "<-" "chan" ) ElementType .

它包括三種類型的定義这橙∽嘁ぃ可選的<-代表channel的方向。如果沒有指定方向屈扎,那么Channel就是雙向的埃唯,既可以接收數(shù)據(jù),也可以發(fā)送數(shù)據(jù)鹰晨。

chan T          // 可以接收和發(fā)送類型為 T 的數(shù)據(jù)
chan<- float64  // 只可以用來發(fā)送 float64 類型的數(shù)據(jù)
<-chan int      // 只可以用來接收 int 類型的數(shù)據(jù)

<-總是優(yōu)先和最左邊的類型結(jié)合墨叛。(The <- operator associates with the leftmost chan possible)

chan<- chan int    // 等價(jià) chan<- (chan int)
chan<- <-chan int  // 等價(jià) chan<- (<-chan int)
<-chan <-chan int  // 等價(jià) <-chan (<-chan int)
chan (<-chan int)

使用make初始化Channel,并且可以設(shè)置容量:

make(chan int, 100)

容量(capacity)代表Channel容納的最多的元素的數(shù)量,代表Channel的緩存的大小并村。
如果沒有設(shè)置容量巍实,或者容量設(shè)置為0, 說明Channel沒有緩存,只有sender和receiver都準(zhǔn)備好了后它們的通訊(communication)才會(huì)發(fā)生(Blocking)哩牍。如果設(shè)置了緩存棚潦,就有可能不發(fā)生阻塞, 只有buffer滿了后 send才會(huì)阻塞膝昆, 而只有緩存空了后receive才會(huì)阻塞丸边。一個(gè)nil channel不會(huì)通信。

可以通過內(nèi)建的close方法可以關(guān)閉Channel荚孵。

你可以在多個(gè)goroutine從/往 一個(gè)channel 中 receive/send 數(shù)據(jù), 不必考慮額外的同步措施妹窖。

Channel可以作為一個(gè)先入先出(FIFO)的隊(duì)列,接收的數(shù)據(jù)和發(fā)送的數(shù)據(jù)的順序是一致的收叶。

channel的 receive支持 multi-valued assignment骄呼,如:

v, ok := <-ch

它可以用來檢查Channel是否已經(jīng)被關(guān)閉了。

send語句
send語句用來往Channel中發(fā)送數(shù)據(jù)判没, 如ch <- 3蜓萄。
它的定義如下:

SendStmt = Channel "<-" Expression .
Channel  = Expression .

在通訊(communication)開始前channel和expression必選先求值出來(evaluated),比如下面的(3+4)先計(jì)算出7然后再發(fā)送給channel澄峰。

c := make(chan int)
defer close(c)
go func() { c <- 3 + 4 }()
i := <-c
fmt.Println(i)

send被執(zhí)行前(proceed)通訊(communication)一直被阻塞著嫉沽。如前所言,無緩存的channel只有在receiver準(zhǔn)備好后send才被執(zhí)行俏竞。如果有緩存绸硕,并且緩存未滿堂竟,則send會(huì)被執(zhí)行。

往一個(gè)已經(jīng)被close的channel中繼續(xù)發(fā)送數(shù)據(jù)會(huì)導(dǎo)致run-time panic玻佩。

往nil channel中發(fā)送數(shù)據(jù)會(huì)一致被阻塞著出嘹。

receive 操作符
<-ch用來從channel ch中接收數(shù)據(jù),這個(gè)表達(dá)式會(huì)一直被block,直到有數(shù)據(jù)可以接收咬崔。
從一個(gè)nil channel中接收數(shù)據(jù)會(huì)一直被block疚漆。

從一個(gè)被close的channel中接收數(shù)據(jù)不會(huì)被阻塞,而是立即返回刁赦,接收完已發(fā)送的數(shù)據(jù)后會(huì)返回元素類型的零值(zero value)。

如前所述闻镶,你可以使用一個(gè)額外的返回參數(shù)來檢查channel是否關(guān)閉甚脉。

x, ok := <-ch
x, ok = <-ch
var x, ok = <-ch

如果OK 是false,表明接收的x是產(chǎn)生的零值铆农,這個(gè)channel被關(guān)閉了或者為空牺氨。

blocking


缺省情況下,發(fā)送和接收會(huì)一直阻塞著墩剖,直到另一方準(zhǔn)備好猴凹。這種方式可以用來在gororutine中進(jìn)行同步,而不必使用顯示的鎖或者條件變量岭皂。

如官方的例子中x, y := <-c, <-c這句會(huì)一直等待計(jì)算結(jié)果發(fā)送到channel中郊霎。

import "fmt"
func sum(s []int, c chan int) {
    sum := 0
    for _, v := range s {
        sum += v
    }
    c <- sum // send sum to c
}
func main() {
    s := []int{7, 2, 8, -9, 4, 0}
    c := make(chan int)
    go sum(s[:len(s)/2], c)
    go sum(s[len(s)/2:], c)
    x, y := <-c, <-c // receive from c
    fmt.Println(x, y, x+y)
}

Buffered Channels


make的第二個(gè)參數(shù)指定緩存的大小:ch := make(chan int, 100)爷绘。

通過緩存的使用书劝,可以盡量避免阻塞,提供應(yīng)用的性能土至。

Range


for …… range語句可以處理Channel购对。

func main() {
    go func() {
        time.Sleep(1 * time.Hour)
    }()
    c := make(chan int)
    go func() {
        for i := 0; i < 10; i = i + 1 {
            c <- i
        }
        close(c)
    }()
    for i := range c {
        fmt.Println(i)
    }
    fmt.Println("Finished")
}

range c產(chǎn)生的迭代值為Channel中發(fā)送的值,它會(huì)一直迭代直到channel被關(guān)閉陶因。上面的例子中如果把close(c)注釋掉骡苞,程序會(huì)一直阻塞在for …… range那一行。

select


select語句選擇一組可能的send操作和receive操作去處理楷扬。它類似switch,但是只是用來處理通訊(communication)操作解幽。
它的case可以是send語句,也可以是receive語句毅否,亦或者default亚铁。

receive語句可以將值賦值給一個(gè)或者兩個(gè)變量。它必須是一個(gè)receive操作螟加。

最多允許有一個(gè)default case,它可以放在case列表的任何位置徘溢,盡管我們大部分會(huì)將它放在最后吞琐。

import "fmt"
func fibonacci(c, quit chan int) {
    x, y := 0, 1
    for {
        select {
        case c <- x:
            x, y = y, x+y
        case <-quit:
            fmt.Println("quit")
            return
        }
    }
}
func main() {
    c := make(chan int)
    quit := make(chan int)
    go func() {
        for i := 0; i < 10; i++ {
            fmt.Println(<-c)
        }
        quit <- 0
    }()
    fibonacci(c, quit)
}

如果有同時(shí)多個(gè)case去處理,比如同時(shí)有多個(gè)channel可以接收數(shù)據(jù),那么Go會(huì)偽隨機(jī)的選擇一個(gè)case處理(pseudo-random)然爆。如果沒有case需要處理站粟,則會(huì)選擇default去處理,如果default case存在的情況下曾雕。如果沒有default case奴烙,則select語句會(huì)阻塞,直到某個(gè)case需要處理剖张。

需要注意的是切诀,nil channel上的操作會(huì)一直被阻塞,如果沒有default case,只有nil channel的select會(huì)一直被阻塞搔弄。

select語句和switch語句一樣幅虑,它不是循環(huán),它只會(huì)選擇一個(gè)case來處理顾犹,如果想一直處理channel倒庵,你可以在外面加一個(gè)無限的for循環(huán):

for {
    select {
    case c <- x:
        x, y = y, x+y
    case <-quit:
        fmt.Println("quit")
        return
    }
}

timeout


select有很重要的一個(gè)應(yīng)用就是超時(shí)處理。 因?yàn)樯厦嫖覀兲岬届潘ⅲ绻麤]有case需要處理擎宝,select語句就會(huì)一直阻塞著。這時(shí)候我們可能就需要一個(gè)超時(shí)操作浑玛,用來處理超時(shí)的情況绍申。
下面這個(gè)例子我們會(huì)在2秒后往channel c1中發(fā)送一個(gè)數(shù)據(jù),但是select設(shè)置為1秒超時(shí),因此我們會(huì)打印出timeout 1,而不是result 1

import "time"
import "fmt"
func main() {
    c1 := make(chan string, 1)
    go func() {
        time.Sleep(time.Second * 2)
        c1 <- "result 1"
    }()
    select {
    case res := <-c1:
        fmt.Println(res)
    case <-time.After(time.Second * 1):
        fmt.Println("timeout 1")
    }
}

其實(shí)它利用的是time.After方法锄奢,它返回一個(gè)類型為<-chan Time的單向的channel失晴,在指定的時(shí)間發(fā)送一個(gè)當(dāng)前時(shí)間給返回的channel中。

Timer和Ticker

我們看一下關(guān)于時(shí)間的兩個(gè)Channel拘央。
timer是一個(gè)定時(shí)器涂屁,代表未來的一個(gè)單一事件,你可以告訴timer你要等待多長(zhǎng)時(shí)間灰伟,它提供一個(gè)Channel拆又,在將來的那個(gè)時(shí)間那個(gè)Channel提供了一個(gè)時(shí)間值。下面的例子中第二行會(huì)阻塞2秒鐘左右的時(shí)間栏账,直到時(shí)間到了才會(huì)繼續(xù)執(zhí)行帖族。

timer1 := time.NewTimer(time.Second * 2)
<-timer1.C
fmt.Println("Timer 1 expired")

當(dāng)然如果你只是想單純的等待的話,可以使用time.Sleep來實(shí)現(xiàn)挡爵。

你還可以使用timer.Stop來停止計(jì)時(shí)器竖般。

timer2 := time.NewTimer(time.Second)
go func() {
    <-timer2.C
    fmt.Println("Timer 2 expired")
}()
stop2 := timer2.Stop()
if stop2 {
    fmt.Println("Timer 2 stopped")
}

ticker是一個(gè)定時(shí)觸發(fā)的計(jì)時(shí)器,它會(huì)以一個(gè)間隔(interval)往Channel發(fā)送一個(gè)事件(當(dāng)前時(shí)間)茶鹃,而Channel的接收者可以以固定的時(shí)間間隔從Channel中讀取事件涣雕。下面的例子中ticker每500毫秒觸發(fā)一次艰亮,你可以觀察輸出的時(shí)間。

ticker := time.NewTicker(time.Millisecond * 500)
go func() {
    for t := range ticker.C {
        fmt.Println("Tick at", t)
    }
}()

類似timer, ticker也可以通過Stop方法來停止挣郭。一旦它停止迄埃,接收者不再會(huì)從channel中接收數(shù)據(jù)了。

close


內(nèi)建的close方法可以用來關(guān)閉channel兑障。

總結(jié)一下channel關(guān)閉后sender的receiver操作侄非。
如果channel c已經(jīng)被關(guān)閉,繼續(xù)往它發(fā)送數(shù)據(jù)會(huì)導(dǎo)致panic: send on closed channel:

import "time"
func main() {
    go func() {
        time.Sleep(time.Hour)
    }()
    c := make(chan int, 10)
    c <- 1
    c <- 2
    close(c)
    c <- 3
}

但是從這個(gè)關(guān)閉的channel中不但可以讀取出已發(fā)送的數(shù)據(jù),還可以不斷的讀取零值:

c := make(chan int, 10)
c <- 1
c <- 2
close(c)
fmt.Println(<-c) //1
fmt.Println(<-c) //2
fmt.Println(<-c) //0
fmt.Println(<-c) //0

但是如果通過range讀取流译,channel關(guān)閉后for循環(huán)會(huì)跳出

c := make(chan int, 10)
c <- 1
c <- 2
close(c)
for i := range c {
    fmt.Println(i)
}

通過i, ok := <-c可以查看Channel的狀態(tài)逞怨,判斷值是零值還是正常讀取的值。

c := make(chan int, 10)
close(c)
i, ok := <-c
fmt.Printf("%d, %t", i, ok) //0, false

同步


channel可以用在goroutine之間的同步福澡。
下面的例子中main goroutine通過done channel等待worker完成任務(wù)骇钦。 worker做完任務(wù)后只需往channel發(fā)送一個(gè)數(shù)據(jù)就可以通知main goroutine任務(wù)完成。

import (
    "fmt"
    "time"
)
func worker(done chan bool) {
    time.Sleep(time.Second)
    // 通知任務(wù)已完成
    done <- true
}
func main() {
    done := make(chan bool, 1)
    go worker(done)
    // 等待任務(wù)完成
    <-done
}

轉(zhuǎn)自:https://colobu.com/2016/04/14/Golang-Channels/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末竞漾,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子窥翩,更是在濱河造成了極大的恐慌业岁,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,548評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件寇蚊,死亡現(xiàn)場(chǎng)離奇詭異笔时,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)仗岸,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,497評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門允耿,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人扒怖,你說我怎么就攤上這事较锡。” “怎么了盗痒?”我有些...
    開封第一講書人閱讀 167,990評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵蚂蕴,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我俯邓,道長(zhǎng)骡楼,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,618評(píng)論 1 296
  • 正文 為了忘掉前任稽鞭,我火速辦了婚禮鸟整,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘朦蕴。我一直安慰自己篮条,他們只是感情好弟头,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,618評(píng)論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著兑燥,像睡著了一般亮瓷。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上降瞳,一...
    開封第一講書人閱讀 52,246評(píng)論 1 308
  • 那天嘱支,我揣著相機(jī)與錄音,去河邊找鬼挣饥。 笑死除师,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的扔枫。 我是一名探鬼主播汛聚,決...
    沈念sama閱讀 40,819評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼短荐!你這毒婦竟也來了倚舀?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,725評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤忍宋,失蹤者是張志新(化名)和其女友劉穎痕貌,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體糠排,經(jīng)...
    沈念sama閱讀 46,268評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡舵稠,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,356評(píng)論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了入宦。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片哺徊。...
    茶點(diǎn)故事閱讀 40,488評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖乾闰,靈堂內(nèi)的尸體忽然破棺而出落追,到底是詐尸還是另有隱情,我是刑警寧澤涯肩,帶...
    沈念sama閱讀 36,181評(píng)論 5 350
  • 正文 年R本政府宣布淋硝,位于F島的核電站,受9級(jí)特大地震影響宽菜,放射性物質(zhì)發(fā)生泄漏谣膳。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,862評(píng)論 3 333
  • 文/蒙蒙 一铅乡、第九天 我趴在偏房一處隱蔽的房頂上張望继谚。 院中可真熱鬧,春花似錦阵幸、人聲如沸花履。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,331評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽诡壁。三九已至济瓢,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間妹卿,已是汗流浹背旺矾。 一陣腳步聲響...
    開封第一講書人閱讀 33,445評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留夺克,地道東北人箕宙。 一個(gè)月前我還...
    沈念sama閱讀 48,897評(píng)論 3 376
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像铺纽,于是被迫代替她去往敵國(guó)和親柬帕。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,500評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容