Go 通道是糟糕的,你應(yīng)該也覺(jué)得很糟糕
更新:如果你是從一篇題為 《糟糕的 Go 語(yǔ)言》 的匯編文章看到這篇博文的話,那么我想表明的是,我很慚愧被列在這樣的名單上。Go 絕對(duì)是我使用過(guò)的最不糟糕的的編程語(yǔ)言。在我寫作本文時(shí),我是想遏制我所看到的一種趨勢(shì),那就是過(guò)度使用 Go 的一些較復(fù)雜的部分。我仍然認(rèn)為 通道可以更好,但是總體而言,Go 很棒。這就像你最喜歡的工具箱中有 這個(gè)工具;它可以有用途(甚至還可能有更多的用途),它仍然可以成為你最喜歡的工具箱!
更新 2:如果我沒(méi)有指出這項(xiàng)對(duì)真實(shí)問(wèn)題的優(yōu)秀調(diào)查,那我將是失職的:《理解 Go 中的實(shí)際并發(fā)錯(cuò)誤》。這項(xiàng)調(diào)查的一個(gè)重要發(fā)現(xiàn)是...Go 通道會(huì)導(dǎo)致很多錯(cuò)誤。
從 2010 年中后期開始,我就斷斷續(xù)續(xù)地在使用 Google 的 Go 編程語(yǔ)言,自 2012 年 1 月開始(在 Go 1.0 之前!),我就用 Go 為 Space Monkey 編寫了合規(guī)的產(chǎn)品代碼。我對(duì) Go 的最初體驗(yàn)可以追溯到我在研究 Hoare 的 通信順序進(jìn)程 并發(fā)模型和 Matt Might 的 UCombinator 研究組 下的 π-演算 時(shí),作為我(現(xiàn)在已重定向)博士工作的一部分,以更好地支持多核開發(fā)。Go 就是在那時(shí)發(fā)布的(多么巧合?。。耶?dāng)即就開始學(xué)習(xí)嘗試了。
它很快就成為了 Space Monkey 開發(fā)的核心部分。目前,我們?cè)?Space Monkey 的生產(chǎn)系統(tǒng)有超過(guò) 42.5 萬(wàn)行的純 Go 代碼(不 包括我們所有的 vendored 庫(kù)中的代碼量,這將使它接近 150 萬(wàn)行),所以也并不是你見過(guò)的最多的 Go 代碼,但是對(duì)于相對(duì)年輕的語(yǔ)言,我們是重度用戶。我們之前 寫了我們的 Go 使用情況。也開源了一些使用率很高的庫(kù);許多人似乎是我們的 OpenSSL 綁定(比 crypto/tls 更快,但請(qǐng)保持 openssl 本身是最新的!)、我們的 錯(cuò)誤處理庫(kù)、日志庫(kù) 和 度量標(biāo)準(zhǔn)收集庫(kù)/zipkin 客戶端 的粉絲。我們使用 Go、我們熱愛 Go、我們認(rèn)為它是目前為止我們使用過(guò)的最不糟糕的、符合我們需求的編程語(yǔ)言。
盡管我也不認(rèn)為我能說(shuō)服自己不要提及我的廣泛避免使用 goroutine-local-storage 庫(kù) (盡管它是一個(gè)你不應(yīng)該使用的魔改技巧,但它是一個(gè)漂亮的魔改),希望我的其他經(jīng)歷足以證明我在解釋我故意煽動(dòng)性的帖子標(biāo)題之前知道我在說(shuō)什么。
等等,什么?
如果你在大街上問(wèn)一個(gè)有名的程序員,Go 有什么特別之處? 她很可能會(huì)告訴你 Go 最出名的是通道 和 goroutine。 Go 的理論基礎(chǔ)很大程度上是建立在 Hoare 的 CSP(通信順序進(jìn)程)模型上的,該模型本身令人著迷且有趣,我堅(jiān)信,到目前為止,它產(chǎn)生的收益遠(yuǎn)遠(yuǎn)超過(guò)了我們的預(yù)期。
CSP(和 π-演算)都使用通信作為核心同步原語(yǔ),因此 Go 會(huì)有通道是有道理的。Rob Pike 對(duì) CSP 著迷(有充分的理由)相當(dāng)深 已經(jīng)有一段時(shí)間了。(當(dāng)時(shí) 和 現(xiàn)在)。
但是從務(wù)實(shí)的角度來(lái)看(也是 Go 引以為豪的),Go 把通道搞錯(cuò)了。在這一點(diǎn)上,通道的實(shí)現(xiàn)在我的書中幾乎是一個(gè)堅(jiān)實(shí)的反模式。為什么這么說(shuō)呢?親愛的讀者,讓我細(xì)數(shù)其中的方法。
你可能最終不會(huì)只使用通道
Hoare 的 “通信順序進(jìn)程” 是一種計(jì)算模型,實(shí)際上,唯一的同步原語(yǔ)是在通道上發(fā)送或接收的。一旦使用 互斥量、信號(hào)量 或 條件變量、bam,你就不再處于純 CSP 領(lǐng)域。 Go 程序員經(jīng)常通過(guò)高呼 “通過(guò)交流共享內(nèi)存” 的 緩存的思想 來(lái)宣揚(yáng)這種模式和哲學(xué)。
那么,讓我們嘗試在 Go 中僅使用 CSP 編寫一個(gè)小程序!讓我們成為高分接收者。我們要做的就是跟蹤我們看到的最大的高分值。如此而已。
首先,我們將創(chuàng)建一個(gè) Game
結(jié)構(gòu)體。
type Game struct {
bestScore int
scores chan int
}
bestScore
不會(huì)受到互斥量的保護(hù)!這很好,因?yàn)槲覀冎恍枰粋€(gè) goroutine 來(lái)管理其狀態(tài)并通過(guò)通道來(lái)接收新的分值即可。
func (g *Game) run() {
for score := range g.scores {
if g.bestScore < score {
g.bestScore = score
}
}
}
好的,現(xiàn)在我們將創(chuàng)建一個(gè)有用的構(gòu)造函數(shù)來(lái)開始 Game
。
func NewGame() (g *Game) {
g = &Game{
bestScore: 0,
scores: make(chan int),
}
go g.run()
return g
}
接下來(lái),假設(shè)有人給了我們一個(gè)可以返回分?jǐn)?shù)的 Player
。它也可能會(huì)返回錯(cuò)誤,因?yàn)榭赡軅魅氲?TCP 流可能會(huì)死掉或發(fā)生某些故障,或者玩家退出。
type Player interface {
NextScore() (score int, err error)
}
為了處理 Player
,我們假設(shè)所有錯(cuò)誤都是致命的,并將獲得的比分向下傳遞到通道。
func (g *Game) HandlePlayer(p Player) error {
for {
score, err := p.NextScore()
if err != nil {
return err
}
g.scores <- score
}
}
好極了!現(xiàn)在我們有了一個(gè) Game
類型,可以以線程安全的方式跟蹤 Player
獲得的最高分?jǐn)?shù)。
你圓滿完成了自己的開發(fā)工作,并開始擁有客戶。你將這個(gè)游戲服務(wù)器公開,就取得了令人難以置信的成功!你的游戲服務(wù)器上也許正在創(chuàng)建許多游戲。
很快,你發(fā)現(xiàn)人們有時(shí)會(huì)離開你的游戲。許多游戲不再有任何玩家在玩,但沒(méi)有任何東西可以阻止游戲運(yùn)行的循環(huán)。死掉的 (*Game).run
goroutines 讓你不知所措。
挑戰(zhàn): 在無(wú)需互斥量或 panics 的情況下修復(fù)上面的 goroutine 泄漏。實(shí)際上,可以滾動(dòng)到上面的代碼,并想出一個(gè)僅使用通道來(lái)解決此問(wèn)題的方案。
我等著。
就其價(jià)值而言,它完全可以只通過(guò)通道來(lái)完成,但是請(qǐng)觀察以下解決方案的簡(jiǎn)單性,它甚至沒(méi)有這個(gè)問(wèn)題:
type Game struct {
mtx sync.Mutex
bestScore int
}
func NewGame() *Game {
return &Game{}
}
func (g *Game) HandlePlayer(p Player) error {
for {
score, err := p.NextScore()
if err != nil {
return err
}
g.mtx.Lock()
if g.bestScore < score {
g.bestScore = score
}
g.mtx.Unlock()
}
}
你想選擇哪一個(gè)?不要被欺騙了,以為通道的解決方案可以使它在更復(fù)雜的情況下更具可讀性和可理解性。拆解是非常困難的。這種拆解若用互斥量來(lái)做那只是小菜一碟,但最困難的是只使用 Go 專用通道來(lái)解決。另外,如果有人回復(fù)說(shuō)發(fā)送通道的通道更容易推理,我馬上就是感到頭疼。
重要的是,這個(gè)特殊的情況可能真的 很容易 解決,而通道有一些運(yùn)行時(shí)的幫助,而 Go 沒(méi)有提供!不幸的是,就目前的情況來(lái)看,與 Go 的 CSP 版本相比,使用傳統(tǒng)的同步原語(yǔ)可以更好地解決很多問(wèn)題,這是令人驚訝的。稍后,我們將討論 Go 可以做些什么來(lái)簡(jiǎn)化此案例。
練習(xí): 還在懷疑? 試著讓上面兩種解決方案(只使用通道與只使用互斥量channel-only vs mutex-only)在一旦 bestScore
大于或等于 100 時(shí),就停止向 Players
索要分?jǐn)?shù)。繼續(xù)打開你的文本編輯器。這是一個(gè)很小的玩具問(wèn)題。
這里的總結(jié)是,如果你想做任何實(shí)際的事情,除了通道之外,你還會(huì)使用傳統(tǒng)的同步原語(yǔ)。
通道比你自己實(shí)現(xiàn)要慢一些
Go 如此重視 CSP 理論,我認(rèn)為其中一點(diǎn)就是,運(yùn)行時(shí)應(yīng)該可以通過(guò)通道做一些殺手級(jí)的調(diào)度優(yōu)化。也許通道并不總是最直接的原語(yǔ),但肯定是高效且快速的,對(duì)吧?
正如 Dustin Hiatt 在 Tyler Treat’s post about Go 上指出的那樣,
在幕后,通道使用鎖來(lái)序列化訪問(wèn)并提供線程安全性。 因此,通過(guò)使用通道同步對(duì)內(nèi)存的訪問(wèn),你實(shí)際上就是在使用鎖。 被包裝在線程安全隊(duì)列中的鎖。 那么,與僅僅使用標(biāo)準(zhǔn)庫(kù)
sync
包中的互斥量相比,Go 的花式鎖又如何呢? 以下數(shù)字是通過(guò)使用 Go 的內(nèi)置基準(zhǔn)測(cè)試功能,對(duì)它們的單個(gè)集合連續(xù)調(diào)用 Put 得出的。
> BenchmarkSimpleSet-8 3000000 391 ns/op
> BenchmarkSimpleChannelSet-8 1000000 1699 ns/o
>
無(wú)緩沖通道的情況與此類似,甚至是在爭(zhēng)用而不是串行運(yùn)行的情況下執(zhí)行相同的測(cè)試。
也許 Go 調(diào)度器會(huì)有所改進(jìn),但與此同時(shí),良好的舊互斥量和條件變量是非常好、高效且快速。如果你想要提高性能,請(qǐng)使用久經(jīng)考驗(yàn)的方法。
通道與其他并發(fā)原語(yǔ)組合不佳
好的,希望我已經(jīng)說(shuō)服了你,有時(shí)候,你至少還會(huì)與除了通道之外的原語(yǔ)進(jìn)行交互。標(biāo)準(zhǔn)庫(kù)似乎顯然更喜歡傳統(tǒng)的同步原語(yǔ)而不是通道。
你猜怎么著,正確地將通道與互斥量和條件變量一起使用,其實(shí)是有一定的挑戰(zhàn)性的。
關(guān)于通道的一個(gè)有趣的事情是,通道發(fā)送是同步的,這在 CSP 中是有很大意義的。通道發(fā)送和通道接收的目的是為了成為同步屏蔽,發(fā)送和接收應(yīng)該發(fā)生在同一個(gè)虛擬時(shí)間。如果你是在執(zhí)行良好的 CSP 領(lǐng)域,那就太好了。
實(shí)事求是地說(shuō),Go 通道也有多種緩沖方式。你可以分配一個(gè)固定的空間來(lái)考慮可能的緩沖,以便發(fā)送和接收是不同的事件,但緩沖區(qū)大小是有上限的。Go 并沒(méi)有提供一種方法來(lái)讓你擁有任意大小的緩沖區(qū) —— 你必須提前分配緩沖區(qū)大小。 這很好,我在郵件列表上看到有人在爭(zhēng)論,因?yàn)闊o(wú)論如何內(nèi)存都是有限的。
What。
這是個(gè)糟糕的答案。有各種各樣的理由來(lái)使用一個(gè)任意緩沖的通道。如果我們事先知道所有的事情,為什么還要使用 malloc
呢?
沒(méi)有任意緩沖的通道意味著在 任何 通道上的幼稚發(fā)送可能會(huì)隨時(shí)阻塞。你想在一個(gè)通道上發(fā)送,并在互斥下更新其他一些記賬嗎?小心!你的通道發(fā)送可能被阻塞!
// ...
s.mtx.Lock()
// ...
s.ch <- val // might block!
s.mtx.Unlock()
// ...
這是哲學(xué)家晚餐大戰(zhàn)的秘訣。如果你使用了鎖,則應(yīng)該迅速更新狀態(tài)并釋放它,并且盡可能不要在鎖下做任何阻塞。
有一種方法可以在 Go 中的通道上進(jìn)行非阻塞發(fā)送,但這不是默認(rèn)行為。假設(shè)我們有一個(gè)通道 ch := make(chan int)
,我們希望在其上無(wú)阻塞地發(fā)送值 1
。以下是在不阻塞的情況下你必須要做的最小量的輸入:
select {
case ch <- 1: // it sent
default: // it didn't
}
對(duì)于剛?cè)腴T的 Go程序員來(lái)說(shuō),這并不是自然而然就能想到的事情。
綜上所述,因?yàn)橥ǖ郎系暮芏嗖僮鞫紩?huì)阻塞,所以需要對(duì)哲學(xué)家及其就餐仔細(xì)推理,才能在互斥量的保護(hù)下,成功地將通道操作與之并列使用,而不會(huì)造成死鎖。
嚴(yán)格來(lái)說(shuō),回調(diào)更強(qiáng)大,不需要不必要的 goroutines
每當(dāng) API 使用通道時(shí),或者每當(dāng)我指出通道使某些事情變得困難時(shí),總會(huì)有人會(huì)指出我應(yīng)該啟動(dòng)一個(gè) goroutine 來(lái)讀取該通道,并在讀取該通道時(shí)進(jìn)行所需的任何轉(zhuǎn)換或修復(fù)。
呃,不。如果我的代碼位于熱路徑中怎么辦?需要通道的實(shí)例很少,如果你的 API 可以設(shè)計(jì)為使用互斥量、信號(hào)量和回調(diào),而不使用額外的 goroutine (因?yàn)樗惺录吘壎际怯?API 事件觸發(fā)的),那么使用通道會(huì)迫使我在資源使用中添加另一個(gè)內(nèi)存分配堆棧。是的,goroutine 比線程輕得多,但更輕量并不意味著是最輕量。
正如我以前 在一篇關(guān)于使用通道的文章的評(píng)論中爭(zhēng)論過(guò)的(呵呵,互聯(lián)網(wǎng)),如果你使用回調(diào)而不是通道,你的 API 總是 可以更通用,總是 更靈活,而且占用的資源也會(huì)大大減少。“總是” 是一個(gè)可怕的詞,但我在這里是認(rèn)真的。有證據(jù)級(jí)的東西在進(jìn)行。
如果有人向你提供了一個(gè)基于回調(diào)的 API,而你需要一個(gè)通道,你可以提供一個(gè)回調(diào),在通道上發(fā)送,開銷不大,靈活性十足。
另一方面,如果有人提供了一個(gè)基于通道的 API 給你,而你需要一個(gè)回調(diào),你必須啟動(dòng)一個(gè) goroutine 來(lái)讀取通道,并且 你必須希望當(dāng)你完成讀取時(shí),沒(méi)有人試圖在通道上發(fā)送更多的東西,這樣你就會(huì)導(dǎo)致阻塞的 goroutine 泄漏。
對(duì)于一個(gè)超級(jí)簡(jiǎn)單的實(shí)際例子,請(qǐng)查看 context 接口(順便說(shuō)一下,它是一個(gè)非常有用的包,你應(yīng)該用它來(lái)代替 goroutine 本地存儲(chǔ))。
type Context interface {
...
// Done returns a channel that closes when this work unit should be canceled.
// Done 返回一個(gè)通道,該通道在應(yīng)該取消該工作單元時(shí)關(guān)閉。
Done() <-chan struct{}
// Err returns a non-nil error when the Done channel is closed
// 當(dāng) Done 通道關(guān)閉時(shí),Err 返回一個(gè)非 nil 錯(cuò)誤
Err() error
...
}
想象一下,你要做的只是在 Done()
通道觸發(fā)時(shí)記錄相應(yīng)的錯(cuò)誤。你該怎么辦?如果你沒(méi)有在通道中選擇的好地方,則必須啟動(dòng) goroutine 進(jìn)行處理:
go func() {
<-ctx.Done()
logger.Errorf("canceled: %v", ctx.Err())
}()
如果 ctx
在不關(guān)閉返回 Done()
通道的情況下被垃圾回收怎么辦?哎呀!這正是一個(gè) goroutine 泄露!
現(xiàn)在假設(shè)我們更改了 Done
的簽名:
// Done calls cb when this work unit should be canceled.
Done(cb func())
首先,現(xiàn)在日志記錄非常容易。看看:ctx.Done(func() { log.Errorf ("canceled:%v", ctx.Err()) })
。但是假設(shè)你確實(shí)需要某些選擇行為。你可以這樣調(diào)用它:
ch := make(chan struct{})
ctx.Done(func() { close(ch) })
瞧!通過(guò)使用回調(diào),不會(huì)失去表現(xiàn)力。 ch
的工作方式類似于用于返回的通道 Done()
,在日志記錄的情況下,我們不需要啟動(dòng)整個(gè)新堆棧。我必須保留堆棧跟蹤信息(如果我們的日志包傾向于使用它們);我必須避免將其他堆棧分配和另一個(gè) goroutine 分配給調(diào)度程序。
下次你使用通道時(shí),問(wèn)問(wèn)你自己,如果你用互斥量和條件變量代替,是否可以消除一些 goroutine ? 如果答案是肯定的,那么修改這些代碼將更加有效。而且,如果你試圖使用通道只是為了在集合中使用 range
關(guān)鍵字,那么我將不得不請(qǐng)你放下鍵盤,或者只是回去編寫 Python 書籍。
通道 API 不一致,只是 cray-cray
在通道已關(guān)閉的情況下,執(zhí)行關(guān)閉或發(fā)送消息將會(huì)引發(fā) panics!為什么呢? 如果想要關(guān)閉通道,你需要在外部同步它的關(guān)閉狀態(tài)(使用互斥量等,這些互斥量的組合不是很好?。@樣其他寫入者才不會(huì)寫入或關(guān)閉已關(guān)閉的通道,或者只是向前沖,關(guān)閉或?qū)懭胍殃P(guān)閉的通道,并期望你必須恢復(fù)所有引發(fā)的 panics。
這是多么怪異的行為。 Go 中幾乎所有其他操作都有避免 panic 的方法(例如,類型斷言具有 , ok =
模式),但是對(duì)于通道,你只能自己動(dòng)手處理它。
好吧,所以當(dāng)發(fā)送失敗時(shí),通道會(huì)出現(xiàn) panic。我想這是有一定道理的。但是,與幾乎所有其他帶有 nil 值的東西不同,發(fā)送到 nil 通道不會(huì)引發(fā) panic。相反,它將永遠(yuǎn)阻塞!這很違反直覺(jué)。這可能是有用的行為,就像在你的除草器上附加一個(gè)開罐器,可能有用(在 Skymall 可以找到)一樣,但這肯定是意想不到的。與 nil 映射(執(zhí)行隱式指針解除引用),nil 接口(隱式指針解除引用),未經(jīng)檢查的類型斷言以及其他所有類型交互不同,nil 通道表現(xiàn)出實(shí)際的通道行為,就好像為該操作實(shí)例化了一個(gè)全新的通道一樣。
接收的情況稍微好一點(diǎn)。在已關(guān)閉的通道上執(zhí)行接收會(huì)發(fā)生什么?好吧,那會(huì)是有效操作——你將得到一個(gè)零值。好吧,我想這是有道理的。獎(jiǎng)勵(lì)!接收允許你在收到值時(shí)進(jìn)行 , ok =
樣式的檢查,以確定通道是否打開。謝天謝地,我們?cè)谶@里得到了 , ok =
。
但是,如果你從 nil 渠道接收會(huì)發(fā)生什么呢? 也是永遠(yuǎn)阻塞! 耶!不要試圖利用這樣一個(gè)事實(shí):如果你關(guān)閉了通道,那么你的通道是 nil!
通道有什么好處?
當(dāng)然,通道對(duì)于某些事情是有好處的(畢竟它們是一個(gè)通用容器),有些事情你只能用它們來(lái)做(比如 select
)。
它們是另一種特殊情況下的通用數(shù)據(jù)結(jié)構(gòu)
Go 程序員已經(jīng)習(xí)慣于對(duì)泛型的爭(zhēng)論,以至于我一提起這個(gè)詞就能感覺(jué)到 PTSD(創(chuàng)傷后應(yīng)激障礙)的到來(lái)。我不是來(lái)談?wù)撨@件事的,所以擦擦額頭上的汗,讓我們繼續(xù)前進(jìn)吧。
無(wú)論你對(duì)泛型的看法是什么,Go 的映射、切片和通道都是支持泛型元素類型的數(shù)據(jù)結(jié)構(gòu),因?yàn)樗鼈円呀?jīng)被特殊封裝到語(yǔ)言中了。
在一種不允許你編寫自己的泛型容器的語(yǔ)言中,任何允許你更好地管理事物集合的東西都是有價(jià)值的。在這里,通道是一個(gè)支持任意值類型的線程安全數(shù)據(jù)結(jié)構(gòu)。
所以這很有用!我想這可以省去一些陳詞濫調(diào)。
我很難把這算作是通道的勝利。
Select
使用通道可以做的主要事情是 select
語(yǔ)句。在這里,你可以等待固定數(shù)量的事件輸入。它有點(diǎn)像 epoll,但你必須預(yù)先知道要等待多少個(gè)套接字。
這是真正有用的語(yǔ)言功能。如果不是 select
,通道將被徹底清洗。但是我的天吶,讓我告訴你,第一次決定可能需要在多個(gè)事物中選擇,但是你不知道有多少項(xiàng),因此必須使用 reflect.Select
。
通道如何才能更好?
很難說(shuō) Go 語(yǔ)言團(tuán)隊(duì)可以為 Go 2.0 做的最具戰(zhàn)術(shù)意義的事情是什么(Go 1.0 兼容性保證很好,但是很費(fèi)勁),但這并不能阻止我提出一些建議。
在條件變量上的 Select !
我們可以不需要通道!這是我提議我們擺脫一些“圣牛”(LCTT 譯注:神圣不可質(zhì)疑的事物)的地方,但是讓我問(wèn)你,如果你可以選擇任何自定義同步原語(yǔ),那會(huì)有多棒?(答:太棒了。)如果有的話,我們根本就不需要通道了。
GC 可以幫助我們嗎?
在第一個(gè)示例中,如果我們能夠使用定向類型的通道垃圾回收(GC)來(lái)幫助我們進(jìn)行清理,我們就可以輕松地解決通道的高分服務(wù)器清理問(wèn)題。
如你所知,Go 具有定向類型的通道。 你可以使用僅支持讀取的通道類型(<-chan
)和僅支持寫入的通道類型(chan<-
)。 這太棒了!
Go 也有垃圾回收功能。 很明顯,某些類型的記賬方式太繁瑣了,我們不應(yīng)該讓程序員去處理它們。 我們清理未使用的內(nèi)存! 垃圾回收非常有用且整潔。
那么,為什么不幫助清理未使用或死鎖的通道讀取呢? 與其讓 make(chan Whatever)
返回一個(gè)雙向通道,不如讓它返回兩個(gè)單向通道(chanReader, chanWriter:= make(chan Type)
)。
讓我們重新考慮一下最初的示例:
type Game struct {
bestScore int
scores chan<- int
}
func run(bestScore *int, scores <-chan int) {
// 我們不會(huì)直接保留對(duì)游戲的引用,因?yàn)檫@樣我們就會(huì)保留著通道的發(fā)送端。
for score := range scores {
if *bestScore < score {
*bestScore = score
}
}
}
func NewGame() (g *Game) {
// 這種 make(chan) 返回風(fēng)格是一個(gè)建議
scoreReader, scoreWriter := make(chan int)
g = &Game{
bestScore: 0,
scores: scoreWriter,
}
go run(&g.bestScore, scoreReader)
return g
}
func (g *Game) HandlePlayer(p Player) error {
for {
score, err := p.NextScore()
if err != nil {
return err
}
g.scores <- score
}
}
如果垃圾回收關(guān)閉了一個(gè)通道,而我們可以證明它永遠(yuǎn)不會(huì)有更多的值,那么這個(gè)解決方案是完全可行的。是的,是的,run
中的評(píng)論暗示著有一把相當(dāng)大的槍瞄準(zhǔn)了你的腳,但至少現(xiàn)在這個(gè)問(wèn)題可以很容易地解決了,而以前確實(shí)不是這樣。此外,一個(gè)聰明的編譯器可能會(huì)做出適當(dāng)?shù)淖C明,以減少這種腳槍造成的損害。
其他小問(wèn)題
- Dup 通道嗎? —— 如果我們可以在通道上使用等效于
dup
的系統(tǒng)調(diào)用,那么我們也可以很容易地解決多生產(chǎn)者問(wèn)題。 每個(gè)生產(chǎn)者可以關(guān)閉自己的dup
版通道,而不會(huì)破壞其他生產(chǎn)者。 - 修復(fù)通道 API! —— 關(guān)閉不是冪等的嗎? 在已關(guān)閉的通道上發(fā)送信息引起的 panics 沒(méi)有辦法避免嗎? 啊!
- 任意緩沖的通道 —— 如果我們可以創(chuàng)建沒(méi)有固定的緩沖區(qū)大小限制的緩沖通道,那么我們可以創(chuàng)建非阻塞的通道。
那我們?cè)撛趺聪虼蠹医榻B Go 呢?
如果你還沒(méi)有,請(qǐng)看看我目前最喜歡的編程文章:《你的函數(shù)是什么顏色》。雖然不是專門針對(duì) Go,但這篇博文比我更有說(shuō)服力地闡述了為什么 goroutines 是 Go 最好的特性(這也是 Go 在某些應(yīng)用程序中優(yōu)于 Rust 的方式之一)。
如果你還在使用這樣的一種編程語(yǔ)言寫代碼,它強(qiáng)迫你使用類似 yield
關(guān)鍵字來(lái)獲得高性能、并發(fā)性或事件驅(qū)動(dòng)的模型,那么你就是活在過(guò)去,不管你或其他人是否知道這一點(diǎn)。到目前為止,Go 是我所見過(guò)的實(shí)現(xiàn) M:N 線程模型(非 1:1 )的語(yǔ)言中最好的入門者之一,而且這種模型非常強(qiáng)大。
所以,跟大家說(shuō)說(shuō) goroutines 吧。
如果非要我選擇 Go 的另一個(gè)主要特性,那就是接口。靜態(tài)類型的 鴨子模型 使得擴(kuò)展、使用你自己或他人的項(xiàng)目變得如此有趣而令人驚奇,這也許值得我改天再寫一組完全不同的文章來(lái)介紹它。
所以…
我一直看到人們爭(zhēng)先恐后沖進(jìn) Go,渴望充分利用通道來(lái)發(fā)揮其全部潛力。這是我對(duì)你的建議。
夠了!
當(dāng)你在編寫 API 和接口時(shí),盡管“絕不”的建議可能很糟糕,但我非常肯定,通道從來(lái)沒(méi)有什么時(shí)候好過(guò),我用過(guò)的每一個(gè)使用通道的 Go API,最后都不得不與之抗?fàn)?。我從?lái)沒(méi)有想過(guò)“哦 太好了,這里是一個(gè)通道;”它總是被一些變體取代,這是什么新鮮的地獄?
所以,請(qǐng)?jiān)谶m當(dāng)?shù)牡胤剑⑶抑辉谶m當(dāng)?shù)牡胤绞褂猛ǖ馈?/em>
在我使用的所有 Go 代碼中,我可以用一只手?jǐn)?shù)出有多少次通道真的是最好的選擇。有時(shí)候是這樣的。那很好!那就用它們吧。但除此之外,就不要再使用了。