Go語言的美好和丑陋解析
這是一個(gè)“Go不好”系列的額外文章。Go確實(shí)有一些不錯(cuò)的特性,也就是本文中“好的”部分,但是當(dāng)我們不使用API或者網(wǎng)絡(luò)服務(wù)器(這是為它設(shè)計(jì)的)而且將它用于業(yè)務(wù)領(lǐng)域邏輯的時(shí)候,總體而言我發(fā)現(xiàn)它用起來笨重且糟糕。但是即使在用于網(wǎng)絡(luò)編程的時(shí)候,在設(shè)計(jì)和實(shí)現(xiàn)方面它也有很多缺陷,這導(dǎo)致它在顯而易見的簡(jiǎn)單的表面之下是危險(xiǎn)的。
促使我寫這篇文章的原因就是最近我又開始用Go做一個(gè)副項(xiàng)目。在我之前的工作我廣泛地使用Go來寫網(wǎng)絡(luò)代理(包括http和原生tcp)來做SaaS服務(wù)。網(wǎng)絡(luò)部分相當(dāng)不錯(cuò)(當(dāng)時(shí)我也是初次嘗試這個(gè)語言),但是賬戶和賬單部分給我?guī)砹送纯?。由于我的副?xiàng)目做的是一個(gè)簡(jiǎn)單的API,我覺得Go應(yīng)該是可以快速完成這個(gè)工作的合適的工具,但是就像我們知道的,很多項(xiàng)目會(huì)擴(kuò)張并超過他們的初始范圍,因此我不得不寫一些數(shù)據(jù)處理來做統(tǒng)計(jì),然后使用Go就又變得痛苦了。因此下面是我對(duì)Go的問題的看法。
一些背景:我喜歡靜態(tài)類型語言。我的第一個(gè)重要項(xiàng)目是用Pascal編寫的。在90年代初我開始工作之時(shí),我使用了Ada和C/C ++。后來我遷移到了Java,最后又使用了Scala(在期間還用過Go),最近開始學(xué)習(xí)Rust。我還寫了大量的JavaScript代碼,因?yàn)橹钡阶罱荳eb瀏覽器中唯一可用的語言。對(duì)動(dòng)態(tài)類型語言我感覺不牢靠,并嘗試將其應(yīng)用限制在簡(jiǎn)單腳本中。我對(duì)命令式、函數(shù)式和面向?qū)ο蟮姆椒ǜ械胶軡M意。
這是篇長(zhǎng)文,所以,這是讓你開胃口的菜單目錄:
好的
- Go易于學(xué)習(xí)
- 易于并發(fā)編程的協(xié)程(goroutines )和通道(channels)
- 強(qiáng)大的標(biāo)準(zhǔn)庫(kù)
- 高性能GO
- 程序語言定義的源代碼格式
- Defer聲明,避免忘記清理
- 新類型
不好之處
- GO忽略現(xiàn)代語言設(shè)計(jì)的進(jìn)步
- 接口是結(jié)構(gòu)類型
- 沒有枚舉
- := / var的困境
- (讓人)恐慌的零值
- Go沒有異常
爛的
- 依賴關(guān)系管理的噩夢(mèng)
- 用語言硬編碼的可變性
- Slice陷阱
- 可變性和渠道:使競(jìng)態(tài)條件(race conditions)很容易
- 混亂的錯(cuò)誤管理
- Nil接口值
- Struct字段標(biāo)記:字符串中的運(yùn)行時(shí)DSL
- 沒有泛型…至少不是為了你
- Go在slice和map之外幾乎沒有什么數(shù)據(jù)結(jié)構(gòu)
- go generate: ok-ish,但是…
結(jié)語
優(yōu)點(diǎn)
Go容易學(xué)習(xí)
這是事實(shí):如果你會(huì)任何一種編程語言,你可以通過“Go教程”在幾個(gè)小時(shí)之內(nèi)學(xué)會(huì)Go的大部分語法,在幾天之內(nèi)就可以寫出你的第一個(gè)程序。閱讀和消化Effective Go,徘徊在標(biāo)準(zhǔn)庫(kù)中,運(yùn)用web工具包如Gorilla 或者Go kit,你就能成為一個(gè)相當(dāng)不錯(cuò)的Go開發(fā)者。
這是因?yàn)镚o的首要目標(biāo)就是簡(jiǎn)單。當(dāng)我開始學(xué)習(xí)Go的時(shí)候,它讓我回憶起了我初次接觸Java:一個(gè)豐富卻不臃腫的簡(jiǎn)單語言。與現(xiàn)在的Java繁重的環(huán)境對(duì)比,學(xué)習(xí)Go是一個(gè)新鮮的體驗(yàn)。由于Go的簡(jiǎn)單,Go程序是非常易讀的,即使錯(cuò)誤處理方面有不少麻煩(這下面更多)。
但是這可能并不是真的簡(jiǎn)單。引用 Rob Pike的話,簡(jiǎn)單即復(fù)雜,我們?cè)谙旅婵梢钥吹皆诤竺嬗泻芏嗟南葳宓戎覀儯?jiǎn)潔和極簡(jiǎn)主義阻止了我們編寫DRY原則的代碼。
使用goroutines 和 channels簡(jiǎn)單的并發(fā)編程
Goroutines可能是Go的最好的特性。與操作系統(tǒng)線程不同,他們是輕量級(jí)的計(jì)算線程。
當(dāng)一個(gè)Go程序執(zhí)行阻塞I/O操作一類的工作時(shí),實(shí)際上Go實(shí)時(shí)掛起了這個(gè)goroutine,而且在一個(gè)event表明一些結(jié)果已經(jīng)可以訪問之后,會(huì)重新運(yùn)行。在此期間,其他goroutines已經(jīng)在為執(zhí)行調(diào)度。因此我們?cè)谑褂靡粋€(gè)同步編程模型做異步編程的時(shí)候有可擴(kuò)展性的優(yōu)點(diǎn)。
Goroutines也是輕量級(jí)的:他們的棧按需增加或減少,也就是說有數(shù)百個(gè)甚至數(shù)千個(gè)goroutines都不是問題。
在一個(gè)應(yīng)用中我曾經(jīng)有一個(gè)goroutine泄露:在結(jié)束之前這些goroutines等待一個(gè)channel去關(guān)閉,但那個(gè)channel不會(huì)關(guān)閉(一個(gè)常見的死鎖問題)。這個(gè)進(jìn)程平白占了90%的CPU,查看expvars顯示60萬個(gè)空的goroutine!我猜CPU都被goroutine調(diào)度占用了。
當(dāng)然,一個(gè)像Akka的actor系統(tǒng)可以不費(fèi)力氣就處理數(shù)百萬actors,一部分是因?yàn)閍ctors沒有棧,但是他們?cè)趯憦?fù)雜并發(fā)request/response應(yīng)用(如 http APIs)時(shí)不如goroutine簡(jiǎn)單的多。
Channels是goroutines之間交互的通道:他們提供了一個(gè)方便的編程模型可以在goroutines之間發(fā)送和接收數(shù)據(jù),而不用依賴脆弱的底層同步原語。Channels擁有他們自己的一套使用模式。
由于錯(cuò)誤的channels數(shù)量(他們默認(rèn)無緩沖)會(huì)導(dǎo)致死鎖,Channels必須要慎重考慮。我們?cè)谙旅嬉矔?huì)提到因?yàn)镚o缺少不變性,使用channels并不能阻止?fàn)帗屬Y源。
強(qiáng)大的標(biāo)準(zhǔn)庫(kù)
Go標(biāo)準(zhǔn)庫(kù)真的很強(qiáng)大,特別是對(duì)網(wǎng)絡(luò)協(xié)議相關(guān)的所有東西或者API開發(fā):http 客戶端和服務(wù)器,加密,壓縮格式,壓縮,發(fā)送郵件等等。甚至還有html解析器和相當(dāng)強(qiáng)大的模板引擎,通過自動(dòng)escaping可以用來產(chǎn)生文字&html來避免XSS(在Hugo 模板的示例中使用)。
大多數(shù)情況下APIs通常是簡(jiǎn)單易懂的。盡管有時(shí)候他們看起來過于簡(jiǎn)單:這當(dāng)中,一部分是由于goroutine編程模式告訴我們只需要關(guān)心“看似同步”的操作。另一部分是因?yàn)樯贁?shù)通用的多功能函數(shù)能替代大量單一功能的函數(shù),就像最近一段時(shí)間,我發(fā)現(xiàn)的那些用于時(shí)間計(jì)算的函數(shù)一樣。
GO是高性能的
Go編譯成一個(gè)本地可執(zhí)行文件。許多Go的用戶來自于Python,Ruby或者Node.js。對(duì)他們來說,這是個(gè)令人興奮的體驗(yàn),因?yàn)樗麄儼l(fā)現(xiàn)服務(wù)器可以處理的并發(fā)請(qǐng)求數(shù)量大幅的增加。對(duì)于沒有并發(fā)的語言(Node.js)或者全局解釋器鎖(GIL)來說,這實(shí)際上是再正常不過的事情。結(jié)合語言的簡(jiǎn)單性,這說明了Go語言令人興奮的一面。
然而相比Java,在原始性能基準(zhǔn)測(cè)試中,情況并不是那么清晰。在內(nèi)存使用和垃圾收集方面Go力壓Java。
Go的垃圾收集的設(shè)計(jì)目的是優(yōu)先考慮延遲和避免stop-the-world停頓,這在服務(wù)器中尤其重要。這可能會(huì)帶來更高的CPU成本,但是在水平可伸縮的架構(gòu)(horizontally scalable architecture)中通過添加更多的機(jī)器這是易于解決的。記住,Go是Google設(shè)計(jì)的,他們不缺資源。
相比于Java,Go的GC在要做的工作方面也更少的:slice的結(jié)構(gòu)是一個(gè)連續(xù)的結(jié)構(gòu)數(shù)組,而不是像Java這樣的指針數(shù)組。相似地,Go的maps出于同樣的目的使用像桶的小數(shù)組。這意味著在GC上工作量更少,并且還更有利于CPU的緩存位置。
Go也可以力壓Java的命令行實(shí)用程序:一個(gè)本地可執(zhí)行的,Go程序?qū)ava的首先必須加載和編譯字節(jié)碼來說沒有啟動(dòng)成本。
語言所定義的源代碼格式
在我職業(yè)生涯中一些最激烈的爭(zhēng)論發(fā)生在團(tuán)隊(duì)代碼格式的定義上。Go通過為Go代碼定義規(guī)范格式解決了這個(gè)問題。gofmt工具會(huì)重新格式化你的代碼,并且沒有選項(xiàng)。
不管喜不喜歡,gofmt都定義了Go代碼應(yīng)該如何格式化,因此該問題得到一次性解決!
標(biāo)準(zhǔn)化的測(cè)試框架
Go在其標(biāo)準(zhǔn)庫(kù)中提供了一個(gè)很好的測(cè)試框架。它支持并行測(cè)試、基準(zhǔn)測(cè)試,并且包含很多用于輕松測(cè)試網(wǎng)絡(luò)客戶端和服務(wù)器的使用程序。
Go程序非常適合運(yùn)維
與Python、Ruby或Node.js相比,僅安裝單個(gè)可執(zhí)行文件對(duì)于運(yùn)維工程師來說是一個(gè)夢(mèng)想。隨著越來越多的Docker投入使用,這個(gè)問題出現(xiàn)的越來越少,但獨(dú)立的可執(zhí)行文件也意味著更小的Docker鏡像。
Go還具有一些內(nèi)置的可觀察性功能,使用expvar包發(fā)布內(nèi)部狀態(tài)和指標(biāo),并且可以輕松添加新內(nèi)容。但要小心,因?yàn)樗鼈冊(cè)谀J(rèn)的http請(qǐng)求處理程序中自動(dòng)暴露,變得不受保護(hù)。Java中JMX有類似的功能,但它更復(fù)雜。
Defer語句,用于避免遺忘清理
defer語句的作用類似于Java中的finally:在當(dāng)前函數(shù)結(jié)束時(shí)執(zhí)行一些清理代碼,并不管此函數(shù)是如何退出的。有關(guān)defer的有趣的事情是它沒有鏈接到一段代碼上,并可以隨時(shí)出現(xiàn)。這允許清理代碼盡可能靠近創(chuàng)建那些需要清理資源的代碼:
- file, err := os.Open(fileName)
- if err != nil {
- return
- }
- defer file.Close()
- // use file, we don't have to think about closing it anymore
當(dāng)然,Java的“try-with-resource”不是那么冗長(zhǎng),同時(shí)Rust在資源的所有者被刪除時(shí)會(huì)自動(dòng)聲明資源,但由于Go要求你對(duì)資源清理明確了解,因此讓它靠近資源分配的地方將其關(guān)閉是很不錯(cuò)的。
自定義類型
我喜歡自定義類型,而且我惱怒/害怕一些情況,就好像當(dāng)我們來回傳一個(gè)字符串型或者long型的持久化對(duì)象標(biāo)識(shí)符的時(shí)候。我們經(jīng)常對(duì)參數(shù)名為id的類型編碼,但是這就是一些產(chǎn)生小bug的原因,即當(dāng)一個(gè)函數(shù)有多個(gè)標(biāo)識(shí)符作為參數(shù)的時(shí)候,一些調(diào)用就會(huì)弄混參數(shù)順序。
Go的自定義類型支持first-class,例如那些分配給一個(gè)已有類型的獨(dú)立的標(biāo)識(shí)符的類型,可以與原來的標(biāo)識(shí)符區(qū)分開來。與封裝相反,自定義類型沒有運(yùn)行時(shí)開銷。這使得編譯器能捕獲這種錯(cuò)誤:
- type UserId string // <-- new type
- type ProductId string
- func AddProduct(userId string, productId string) {}
- func main() {
- userId := UserId("some-user-id")
- productId := ProductId("some-product-id")
- // Right order: all fine
- AddProduct(userId, productId)
- // Wrong order: would compile with raw strings
- AddProduct(productId, userId)
- // Compilation errors:
- // cannot use productId (type ProductId) as type UserId in argument to AddProduct
- // cannot use userId (type UserId) as type ProductId in argument to AddProduct
- }
不幸的是,對(duì)那些要求自定義類型與原始類型做轉(zhuǎn)換的人來說,由于不支持泛型,自定義類型在寫復(fù)用代碼的時(shí)候用起來比較累贅。
不好之處
GO忽略現(xiàn)代語言設(shè)計(jì)的進(jìn)步
在大道至簡(jiǎn)(Less is exponentially more)的演講上,Rob Pike解釋說Go是要取代C和C++的,它的前身是Newsqueak,這是他在80年代寫的一種語言。Go也有很多關(guān)于Plan9的參考,這是一個(gè)分布式操作系統(tǒng),80年代在貝爾實(shí)驗(yàn)室開發(fā)的。
甚至有一個(gè)Go組件直接從Plan9獲得靈感。為什么不使用LLVM來提供范圍廣泛的目標(biāo)體系結(jié)構(gòu)呢?我可能也在這里漏掉了什么,但為什么需要呢?如果你需要編寫程序集以充分利用CPU,那么你不是應(yīng)該直接使用目標(biāo)CPU匯編語言嗎?
Go的設(shè)計(jì)者很值得尊敬,但是他們就像在一個(gè)平行宇宙(或者他們的Plan9實(shí)驗(yàn)室)設(shè)計(jì)的Go,在那里大多數(shù)編譯器和編程語言的設(shè)計(jì)都不是在90年代和2000年代?;蛘呤悄切┠軐懢幾g器的系統(tǒng)編程人員設(shè)計(jì)了Go。
函數(shù)式編程?沒有提到它。泛型?你不需要它們,看看它們?cè)贑++里產(chǎn)生的混亂吧!哪怕slice,map和channels都是泛型類型,就像接下來我們會(huì)看到的。
Go的目標(biāo)就是代替C和C++,但是很明顯它的設(shè)計(jì)者沒有多看看其他語言。他們避開了他們的目標(biāo),Google的C和C++開發(fā)者不采用它。我猜主要原因就是垃圾回收。低級(jí)C開發(fā)者十分抗拒管理內(nèi)存,因?yàn)樗麄儾涣私夤芾硎裁?,在什么時(shí)候管理。他們喜歡這種控制,即使會(huì)帶來額外的復(fù)雜,而且打開內(nèi)存泄露和buffer溢出的大門。有趣的是,Rust在不使用GC的情況下使用另一種方法做自動(dòng)內(nèi)存管理。
相反的,在操作工具方面Go吸引了那些像使用Python和Ruby等腳本語言的人。他們?cè)贕o中發(fā)現(xiàn)一個(gè)方法,有很好的性能,而且減少了內(nèi)存/cpu/硬盤的占用空間。而且也是更static的類型,這對(duì)他們來說是新穎的。對(duì)GO來說Docker是殺手級(jí)應(yīng)用,這使得它在開發(fā)界開始被廣泛使用。Kubernetes的提出加強(qiáng)了這個(gè)趨勢(shì)。
Interfaces是結(jié)構(gòu)化類型(structural types)
Go的interfaces就像Java的interfaces或者Scala和Rust的traits:他們定義行為,之后才會(huì)被一個(gè)type(我在這不把他們叫做“class”)實(shí)現(xiàn)。
不像Java的interfaces和Scala和Rust的traits,一個(gè)type不需要明確定義它實(shí)現(xiàn)了一個(gè)interface:它必須要實(shí)現(xiàn)所有定義在interfaces中的函數(shù)。因此Go的interfaces的確是structural types。
我們也許認(rèn)為Go允許在其他的packages中實(shí)現(xiàn)interface,而不僅僅是在type所在的packages中申請(qǐng),就像Scala、Kotlin的類擴(kuò)展和Rust的trait一樣。但事實(shí)并非如此:與type相關(guān)的所有方法都必須在這個(gè)type的package中定義。
Go并不是唯一使用structural typing的語言,但我發(fā)現(xiàn)它存在幾個(gè)缺點(diǎn):
- 尋找有哪些type實(shí)現(xiàn)了interface是困難的,因?yàn)樗蕾囉诤瘮?shù)定義匹配。在Java或Scala中,我經(jīng)常通過搜索實(shí)現(xiàn)了interface的類來尋找相關(guān)的實(shí)現(xiàn)。
- 當(dāng)給interface添加一個(gè)方法時(shí),你將會(huì)發(fā)現(xiàn)只有當(dāng)那些types被用作interface type的值時(shí),type才會(huì)被更新。很長(zhǎng)一段時(shí)間內(nèi)你會(huì)忽視這種問題。Go建議盡少使用有只有幾個(gè)方法的interfaces,以此來防止該問題的發(fā)生。
- 因?yàn)閠ype中有一個(gè)方法與interface相同,這個(gè)type可能會(huì)無意中實(shí)現(xiàn)了一個(gè)interface。但是偶然的情況下,它所實(shí)現(xiàn)的功能可能與預(yù)想的interface協(xié)議不同。
更新:interface的一些丑陋的地方,請(qǐng)?jiān)斂春竺娴?ldquo;interface空值”章節(jié)。
沒有枚舉類型
Go中沒有枚舉值,在我看來這是一個(gè)錯(cuò)失的機(jī)會(huì)。
iota可以快速生成自增的數(shù)值,但它看起來更像是一種修改而非特性。而實(shí)際上,由于在一系列iota所生成的常量中插入一行會(huì)改變其后面的值是一個(gè)危險(xiǎn)的操作。由于所生成的值是在整個(gè)代碼中使用的,因此這可能會(huì)觸發(fā)意外。
這也意味著在Go中沒有辦法讓編譯器檢查switch語句是否詳盡,并且無法描述給定類型所支持的值。
:= / var 的尷尬
Go提供了兩種方法聲明和分配給變量一個(gè)值:var x = “foo” 和 x := “foo”,為什么這樣?
主要區(qū)別是:var允許聲明而不初始化(那你就必須聲明類型),就像var x string,然而 :=要求分配一個(gè)值,而且這種方法可以同樣用于已有變量和新變量。我猜發(fā)明:=就是用來讓我們?cè)诓东@錯(cuò)誤的時(shí)候不那么痛苦的:
使用var:
- var x, err1 = SomeFunction()
- if (err1 != nil) {
- return nil
- }
- var y, err2 = SomeOtherFunction()
- if (err2 != nil) {
- return nil
- }
使用:= :
- x, err := SomeFunction()
- if (err != nil) {
- return nil
- }
- y, err := SomeOtherFunction()
- if (err != nil) {
- return nil
- }
:=語法也容易不小心對(duì)一個(gè)變量重新賦值。我曾經(jīng)不止一次遇到這個(gè)問題,就像:=(聲明和分配)與=(分配)太像了,就像下面這樣:
- foo := "bar"
- if someCondition {
- foo := "baz"
- doSomething(foo)
- }
- // foo == "bar" even if "someCondition" is true
零值恐慌
Go里沒有構(gòu)造函數(shù)。因此,它奉行“零值”應(yīng)該可以隨時(shí)使用。這是一個(gè)有趣的方法,但在我看來,它所帶來的簡(jiǎn)化化主要是針對(duì)語言實(shí)現(xiàn)者的。
在實(shí)踐中,如果沒有正確的初始化,許多類型都不能做有用的事情。讓我們來看一下在Effective Go中作為示例的io.Fileobject:
- type File struct {
- *file // os specific
- }
- func (f *File) Name() string {
- return f.name
- }
- func (f *File) Read(b []byte) (n int, err error) {
- if err := f.checkValid("read"); err != nil {
- return 0, err
- }
- n, e := f.read(b)
- return n, f.wrapErr("read", e)
- }
- func (f *File) checkValid(op string) error {
- if f == nil {
- return ErrInvalid
- }
- return nil
- }
我們?cè)谶@里能看到什么呢?
- 在零值文件上調(diào)用Name()將會(huì)出現(xiàn)問題,因?yàn)樗膄ile字段為nil。
- Read函數(shù)和File幾乎所有其他的方法都一樣,首先檢查文件是否已初始化。
所以基本上零值文件不僅沒用,而且會(huì)導(dǎo)致問題。你必須使用以下構(gòu)造函數(shù)中的一個(gè):如“Open”或“Create”。檢查是否正確的初始化是每次函數(shù)調(diào)用都必須承受的開銷。
標(biāo)準(zhǔn)庫(kù)中有無數(shù)類似這樣的類型,有些甚至不試圖使用它們的零值做一些有用的事情。在零值的html.Template上調(diào)用任何方法:它們都引起問題。
同時(shí)map的零值有個(gè)嚴(yán)重的缺陷:它可以查詢,但在map中存儲(chǔ)任何數(shù)據(jù)都有導(dǎo)致panic異常:
- var m1 = map[string]string{} // empty map
- var m0 map[string]string // zero map (nil)
- println(len(m1)) // outputs '0'
- println(len(m0)) // outputs '0'
- println(m1["foo"]) // outputs ''
- println(m0["foo"]) // outputs ''
- m1["foo"] = "bar" // ok
- m0["foo"] = "bar" // panics!
當(dāng)結(jié)構(gòu)具有map字段時(shí),就要當(dāng)心了,因?yàn)樵谙蚱涮砑訔l目之前必須對(duì)其進(jìn)行初始化。
因此,身為一名開發(fā)人員,你必須經(jīng)常檢查你要使用的結(jié)構(gòu)體是否需要調(diào)用構(gòu)造函數(shù)或者零值是否有用。為了一些語言上的簡(jiǎn)化,這將給代碼編寫者帶來很大的負(fù)擔(dān)。
Go中沒有異常
博客文章“為何Go處理異常是正確的”中詳細(xì)解釋了為什么異常是很糟糕的,以及為什么Go中的方法需要返回錯(cuò)誤是更好的作法。我同意這一點(diǎn),并且在使用異步編程或像Java流這樣的函數(shù)式風(fēng)格時(shí),異常是很難處理的(讓我們暫且將之拋之腦后,因?yàn)榍罢咴贕o中是不需要的,這要?dú)w功于goroutine;而后者根本不可能)。該博文中提到panic是“對(duì)你的程序總是致命的,游戲結(jié)束”,這是對(duì)的。
現(xiàn)在,“Defer, panic和recove”在這之前,解釋了如何從panic中恢復(fù)過來(實(shí)際上通過捕獲它們),并說:“對(duì)于一個(gè)真實(shí)世界的panic和恢復(fù)示例,請(qǐng)參閱Go標(biāo)準(zhǔn)庫(kù)中的json包。
事實(shí)上,json解碼器有一個(gè)會(huì)觸發(fā)panic的通用的錯(cuò)誤處理函數(shù),在最頂層的unmarshall函數(shù)中可恢復(fù)該panic,該函數(shù)將檢查panic類型,并在其是“local panic”時(shí)將其作為錯(cuò)誤返回,或重新觸發(fā)panic錯(cuò)誤(在此丟失了原來的panic堆棧跟蹤信息)。
對(duì)于任一Java開發(fā)人員來說,這看起來像try / catch (DecodingException ex)。所以Go確實(shí)有異常,僅在內(nèi)部使用但建議你不要使用。
有趣的是:幾個(gè)星期前,一個(gè)非googler修復(fù)了json解碼器,其中使用常規(guī)錯(cuò)誤冒泡。
丑陋的
依賴關(guān)系管理的噩夢(mèng)
一位知名的Google Go開發(fā)者Jaana Dogan(又名JBD),最近在推特上發(fā)泄他的不滿:
如果依賴關(guān)系管理不能在一年解決,我會(huì)考慮棄用Go并且永遠(yuǎn)不再回來。依賴性管理問題通常會(huì)改變我從語言中獲得的所有樂趣。
- 133
- 有44人在談?wù)撨@件事
讓我們把它簡(jiǎn)單化:Go中沒有依賴項(xiàng)管理,所有當(dāng)前的解決方案都只是一些技巧和變通方法。
這可以追溯回谷歌的起源階段,眾所周知,谷歌使用了一個(gè)巨大的單塊存儲(chǔ)庫(kù),用于所有源代碼。不需要模塊版本控制,不需要第三方模塊存儲(chǔ)庫(kù),你在你當(dāng)前分支上build任何(你想要的)東西。不幸的是,這在開放的互聯(lián)網(wǎng)上行不通。
為 Go 添加依賴就表示將依賴項(xiàng)的源代碼庫(kù)拷貝到 GOPATH 中。但是是什么版本呢?是克隆時(shí)的 master 分支,不管它是哪個(gè)版本。如果不同項(xiàng)目需要不同版本的依賴項(xiàng)怎么辦呢?沒辦法。版本的概念甚至不存在。
同時(shí),自己的項(xiàng)目也要放在 GOPATH,否則編譯器就找不到它。你是否想讓項(xiàng)目整潔的組織在各自獨(dú)立的目錄中呢?那就必須想為每個(gè)項(xiàng)目設(shè)置 GOPATH 或恰當(dāng)?shù)慕⒎线B接。
社區(qū)中設(shè)計(jì)出來的方法帶來了大量工具。包管理工具引入了提供方和 lock 文件,它們包含的 Git ShA1 可以支持重復(fù)構(gòu)建。
vendor 目錄最終在 Go 1.6 中得到了官方支持。但對(duì)于克隆的供應(yīng)內(nèi)容,仍然沒有合適的版本管理。也不能通過語義化版本解決混淆導(dǎo)入和依賴傳遞的問題。
不過情況正在好轉(zhuǎn):dep,最近出現(xiàn)了這個(gè)官方依賴管理工具用于支持供應(yīng)內(nèi)容。它支持版本(git tags),同時(shí)具有支持語義化版本約定的版本解析器。這個(gè)工具尚未達(dá)到穩(wěn)定版本,但它在做正確的事情。而且,它仍然需要你把項(xiàng)目放在 GOPATH 目錄下。
但dep可能不會(huì)存在太久,因?yàn)関go,也來自Google,想在語言本身中引入版本信息并且近期一直在發(fā)起一些此類的浪潮。
所以Go中的依賴管理是噩夢(mèng)般的存在。完成配置是很痛苦的,而你在開發(fā)過程中從沒有考慮過它,直到你添加一個(gè)新的導(dǎo)入或者簡(jiǎn)單地想把你的一個(gè)團(tuán)隊(duì)成員的一個(gè)分支拉到你的GOPATH中時(shí)…
現(xiàn)在讓我們回到代碼上吧。
可變性在語言中是硬編碼的
在Go中沒有辦法定義不可變的結(jié)構(gòu)體:struct字段是可變的,而const關(guān)鍵詞不適用于它們。Go可以很容易地通過簡(jiǎn)單的賦值來完成整個(gè)結(jié)構(gòu)的復(fù)制,因此我們可能會(huì)認(rèn)為按值傳參是以復(fù)制為代價(jià)以實(shí)現(xiàn)不變性的前提。
然而,毫不奇怪,這不會(huì)復(fù)制由指針引用的值。由于內(nèi)置集合(map,slice和array)是引用并且是可變的,所以復(fù)制包含其中之一的結(jié)構(gòu)體只會(huì)將復(fù)制指向同一后臺(tái)內(nèi)存的指針。
下面的示例說明這一點(diǎn):
- type S struct {
- A string
- B []string
- }
- func main() {
- x := S{"x-A", []string{"x-B"}}
- y := x // copy the struct
- y.A = "y-A"
- y.B[0] = "y-B"
- fmt.Println(x, y)
- // Outputs "{x-A [y-B]} {y-A [y-B]}" -- x was modified!
- }
所以你必須對(duì)此非常小心,并且如果你是通過傳值來傳遞參數(shù)的話,則不要假定它是不可變的。
有一些deepcopy庫(kù)試圖用(慢)反射來解決這個(gè)問題,但由于專有字段不能被反射訪問,所以它們存在不足之處。因此可避免競(jìng)態(tài)條件的預(yù)防式復(fù)制將會(huì)是很困難的,需要大量的樣板代碼。Go甚至沒有可以標(biāo)準(zhǔn)化的Clone接口。
Slice陷阱
Slices帶來了很多陷阱:就像在“Go slices: usage and internals”中解釋的一樣,由于一些性能原因,re-slicing一個(gè)slice不會(huì)復(fù)制底層數(shù)組。它 的目的是好的,但這意味著一個(gè)slice中的子slice僅僅是繼承了原始slice的mutations的視圖。因此如果你想將子slice與原始的slice區(qū)分,不要忘了copy()這個(gè)slice。
因?yàn)閍ppend函數(shù),忘記調(diào)用copy()會(huì)很危險(xiǎn):如果它沒有足夠的容量存儲(chǔ)新值,在一個(gè)slice中append一個(gè)值會(huì)改變底層數(shù)組的大小。這就意味著append的結(jié)果可能會(huì)也可能不會(huì)指向依賴初始化容量的原始的數(shù)組。這會(huì)導(dǎo)致很難找到不確定的bugs。
在下面的代碼我們看到一個(gè)函數(shù)將值append到一個(gè)子slice改變了使用容量初始化的原始slice產(chǎn)生的結(jié)果。
- func doStuff(value []string) {
- fmt.Printf("value=%v\n", value)
- value2 := value[:]
- value2 = append(value2, "b")
- fmt.Printf("value=%v, value2=%v\n", value, value2)
- value2[0] = "z"
- fmt.Printf("value=%v, value2=%v\n", value, value2)
- }
- func main() {
- slice1 := []string{"a"} // length 1, capacity 1
- doStuff(slice1)
- // Output:
- // value=[a] -- ok
- // value=[a], value2=[a b] -- ok: value unchanged, value2 updated
- // value=[a], value2=[z b] -- ok: value unchanged, value2 updated
- slice10 := make([]string, 1, 10) // length 1, capacity 10
- slice10[0] = "a"
- doStuff(slice10)
- // Output:
- // value=[a] -- ok
- // value=[a], value2=[a b] -- ok: value unchanged, value2 updated
- // value=[z], value2=[z b] -- WTF?!? value changed???
- }
Mutability 和 channels:更容易產(chǎn)生競(jìng)爭(zhēng)條件
Go的并發(fā)是使用channels在CSP上建立的,這會(huì)使相應(yīng)的goroutines比同步共享數(shù)據(jù)更加簡(jiǎn)單和安全。這里的mantra是“不要通過共享內(nèi)存來通信;相反,通過通信來共享內(nèi)存”。然而這只是一廂情愿,實(shí)際上并不能安全的完成這個(gè)目標(biāo)。
就像我們之前看到的,在Go中沒有方法使用不可變數(shù)據(jù)結(jié)構(gòu)。這意味著我們使用channel發(fā)送一個(gè)指針,就玩完了:我們?cè)诓l(fā)進(jìn)程共享了可變數(shù)據(jù)。當(dāng)然structures(不是指針)的一個(gè)channel復(fù)制了在channel上發(fā)送的值,但是就像我們之前看到的,這不是深度復(fù)制引用,包括slices和maps,他們本質(zhì)上都是可變的。一個(gè)interface type的結(jié)構(gòu)字段也是一樣:他們是指針,interface定義的任何mutation方法都是通向競(jìng)爭(zhēng)條件的大門。
因此雖然channels明顯讓并發(fā)編程更簡(jiǎn)單,他們不阻止在共享數(shù)據(jù)里的競(jìng)爭(zhēng)條件。而且slices和maps的本質(zhì)可變性讓這種情況更容易發(fā)生。
來說一下競(jìng)爭(zhēng)條件,Go包含了一個(gè)競(jìng)爭(zhēng)條件的檢測(cè)模式,這些代碼工具是用來尋找未同步的共享訪問。它只能在他們出問題的時(shí)候檢測(cè)競(jìng)爭(zhēng)問題,因此大多都是在集成或負(fù)載測(cè)試中使用,借此期望產(chǎn)生會(huì)引發(fā)競(jìng)爭(zhēng)條件的問題。在生產(chǎn)中,實(shí)際上這并不可行,因?yàn)樗母哌\(yùn)行時(shí)代價(jià),除了臨時(shí)debug sessions。
雜亂的錯(cuò)誤管理
在Go中你需要快速學(xué)習(xí)的是錯(cuò)誤處理模式,因?yàn)榉磸?fù)出現(xiàn):
- someData, err := SomeFunction()
- if err != nil {
- return err;
- }
由于Go聲稱不支持異常(雖然它支持異常),但每個(gè)可能以錯(cuò)誤結(jié)尾的函數(shù)都必須有error作為其最終處理結(jié)果。 這尤其適用于執(zhí)行一些I / O功能,因此這種冗長(zhǎng)的模式在網(wǎng)絡(luò)應(yīng)用程序中非常普遍,這是Go的主要領(lǐng)域。
你的眼睛會(huì)很快為這種模式開發(fā)一個(gè)可視化過濾器,并將其識(shí)別為“是的,錯(cuò)誤處理”,但仍然有很多其他干擾,有時(shí)很難在錯(cuò)誤處理過程中找到實(shí)際的代碼。
雖然有一些陷阱,因?yàn)殄e(cuò)誤結(jié)果實(shí)際上可能只是一個(gè)表面上的情況,例如從普遍存在的io.Reader讀取時(shí):
- len, err := reader.Read(bytes)
- if err != nil {
- if err == io.EOF {
- // All good, end of file
- } else {
- return err
- }
- }
在“有價(jià)值錯(cuò)誤”中,Rob Pike提出了一些減少冗長(zhǎng)錯(cuò)誤處理的策略。 我發(fā)現(xiàn)他們實(shí)際上是危險(xiǎn)的救急:
- type errWriter struct {
- w io.Writer
- err error
- }
- func (ew *errWriter) write(buf []byte) {
- if ew.err != nil {
- return // Write nothing if we already errored-out
- }
- _, ew.err = ew.w.Write(buf)
- }
- func doIt(fd io.Writer) {
- ew := &errWriter{w: fd}
- ew.write(p0[a:b])
- ew.write(p1[c:d])
- ew.write(p2[e:f])
- // and so on
- if ew.err != nil {
- return ew.err
- }
- }
基本上,從以上認(rèn)識(shí)到,檢查錯(cuò)誤提供一種一直令人痛苦的模式,直到結(jié)束時(shí)才忽略寫入序列中的錯(cuò)誤。 因此,即使我們知道它不應(yīng)該執(zhí)行,任何執(zhí)行的操作都會(huì)在執(zhí)行完錯(cuò)誤后執(zhí)行。 如果這些比分片更昂貴呢? 我們只是浪費(fèi)資源,因?yàn)镚o的錯(cuò)誤處理是一件痛苦的事情。
Rust有一個(gè)類似的問題:沒有異常處理(與Go相反,沒有),函數(shù)可能失敗后返回Result ,并且需要對(duì)結(jié)果進(jìn)行一些模式匹配。 所以Rust1.0帶有try! 宏指令認(rèn)識(shí)到這種模式的普遍性,并做成一流的語言功能。 因此,您在保持正確的錯(cuò)誤處理的同時(shí)保持上述代碼的簡(jiǎn)潔。
不幸的是,將Rust的方法轉(zhuǎn)換為Go是不可能的,因?yàn)镚o沒有泛型或宏。
無接口值
在一次更新后,出現(xiàn)redditor jmickeyd顯示nil和接口的奇怪行為,這十分丑陋。 我把它擴(kuò)展了一點(diǎn):
- type Explodes interface {
- Bang()
- Boom()
- }
- // Type Bomb implements Explodes
- type Bomb struct {}
- func (*Bomb) Bang() {}
- func (Bomb) Boom() {}
- func main() {
- var bomb *Bomb = nil
- var explodes Explodes = bomb
- println(bomb, explodes) // '0x0 (0x10a7060,0x0)'
- if explodes != nil {
- explodes.Bang() // works fine
- explodes.Boom() // panic: value method main.Bomb.Boom called using nil *Bomb pointer
- }
- }
上面的代碼驗(yàn)證了explode不是nil,但是code在Boom中冒出來,但不在Bang中。 這是為什么? 解釋一下在println行中:bomb指針是0x0,實(shí)際上是nil,但explodes是非空值(0x10a7060,0x0)。
該pair的第一個(gè)元素是指向由Explodes類型所實(shí)現(xiàn)的Bomb接口的方法的調(diào)度表的指針,第二個(gè)元素是實(shí)際Explodes對(duì)象的地址,該地址為nil。
對(duì)Bang的調(diào)用成功了,因?yàn)樗鼞?yīng)用在指向Bomb的指針上:不需要解引用該指針來調(diào)用該方法。Boom方法操作一個(gè)值,因此一個(gè)調(diào)用導(dǎo)致指針被解引用,這會(huì)導(dǎo)致panic。
請(qǐng)注意,如果我們寫了var explode Explodes = nil,那么!= nil將不會(huì)成功。
那么我們應(yīng)該如何以安全的方式編寫測(cè)試? 我們必須對(duì)接口值和非零值都進(jìn)行nil-check,檢查接口對(duì)象指向的值…使用反射!
- if explodes != nil && !reflect.ValueOf(explodes).IsNil() {
- explodes.Bang() // works fine
- explodes.Boom() // works fine
- }
錯(cuò)誤或功能? Tour of Go有一個(gè)專門的頁面來解釋這種行為,并明確指出:“請(qǐng)注意,一個(gè)具有nil值的接口值本身不是零”。
不過,這很丑陋,可能會(huì)導(dǎo)致很微小的錯(cuò)誤。 它在語言設(shè)計(jì)中看起來像是一個(gè)很大的缺陷,使其實(shí)現(xiàn)更容易。
結(jié)構(gòu)字段標(biāo)簽:運(yùn)行時(shí)字符串中的DSL
如果您在Go中使用過JSON,您肯定遇到過類似的情況:
- type User struct {
- Id string `json:"id"`
- Email string `json:"email"`
- Name string `json:"name,omitempty"`
- }
這些語言規(guī)范所說的結(jié)構(gòu)標(biāo)簽是一個(gè)字符串“通過反射接口可見并參與結(jié)構(gòu)的類型標(biāo)識(shí),但是被忽略”。 所以基本上,寫上任何你想要的字符串,并在運(yùn)行時(shí)使用反射來解析它。 如果語法不對(duì),會(huì)在運(yùn)行時(shí)會(huì)出現(xiàn)宕機(jī)。
這個(gè)字符串實(shí)際上是字段元數(shù)據(jù),在許多語言中已經(jīng)存在了數(shù)十年的“注釋”或“屬性”。 通過語言支持,它們的語法在編譯時(shí)被正式定義和檢查,同時(shí)仍然是可擴(kuò)展的。
為什么Go決定使用原始字符串,并且任何庫(kù)都可以決定是否使用它想要的任何DSL,在運(yùn)行時(shí)解析?
當(dāng)您使用多個(gè)庫(kù)時(shí),情況可能會(huì)變得尷尬:下面是從協(xié)議緩沖區(qū)的Go文檔中取出的一個(gè)例子:
- type Test struct {
- Label *string `protobuf:"bytes,1,req,name=label" json:"label,omitempty"`
- Type *int32 `protobuf:"varint,2,opt,name=type,def=77" json:"type,omitempty"`
- Reps []int64 `protobuf:"varint,3,rep,name=reps" json:"reps,omitempty"`
- Optionalgroup *Test_OptionalGroup `protobuf:"group,4,opt,name=OptionalGroup" json:"optionalgroup,omitempty"`
- }
邊注:為什么在使用JSON的時(shí)候有很多常見的標(biāo)簽。因?yàn)樵贕o中,public的字段必須使用大駱駝命名法,或者至少以大寫字母開始。然而在JSON中,常見的字段命名習(xí)慣用小駱駝命名法或者蛇形命名法。因此需要很多冗長(zhǎng)的標(biāo)簽。
JSON編碼器和解碼器標(biāo)準(zhǔn)不允許提供命名策略來轉(zhuǎn)自動(dòng)轉(zhuǎn)化,就像Java中的Jackson文檔。這就解釋了為什么在Docker APIs的所有的字段都是大駝峰命名法:避免他的開發(fā)人員為他們的大型API寫這些麻煩的標(biāo)簽。
沒有泛型……至少不適合你
很難想象一個(gè)沒有泛型的現(xiàn)代靜態(tài)類型語言,但這就是你用Go得到的東西:它沒有泛型……或者更確切地說幾乎沒有泛型,正如我們將看到的那樣,這使得它比沒有泛型更糟糕。
內(nèi)置切片,地圖,數(shù)組和通道是通用的。 聲明一個(gè)map [string] MyStruct清楚地顯示了使用具有兩個(gè)參數(shù)的泛型類型。 這很好,因?yàn)樗试S類型安全編程捕捉各種錯(cuò)誤。
然而,沒有用戶可定義的泛型數(shù)據(jù)結(jié)構(gòu)。這意味著你無法以類型安全的方式定義可用于任何一個(gè)type的可復(fù)用abstractions。你必須使用untyped的interface{}并且需要將值轉(zhuǎn)成合適的type。任何錯(cuò)誤都只會(huì)在運(yùn)行時(shí)捕獲,并且產(chǎn)生了panic。作為一個(gè)Java開發(fā)者,這就像回到了之前2004年Java5時(shí)代。
在 “Less is exponentially more“中,Rob Pike驚人的將泛型和繼承放進(jìn)了同一個(gè)“typed programming”包中,說他贊成組合替換繼承。不喜歡繼承是可以的(事實(shí)上,我寫Scala的時(shí)候很少使用繼承)但是泛型解決了另一個(gè)問題:在保持類型安全的同時(shí)有可復(fù)用性。
正如接下來我們將看到的,把內(nèi)置的泛型與用戶定義的非泛型分隔開,對(duì)開發(fā)者的“舒適度”和編譯時(shí)的類型安全產(chǎn)生了影響:它影響了整個(gè)Go的生態(tài)系統(tǒng)。
Go除了分片和映射之外幾乎沒有數(shù)據(jù)結(jié)構(gòu)
Go生態(tài)系統(tǒng)沒有很多數(shù)據(jù)結(jié)構(gòu),它們可以從內(nèi)置切片和貼圖中提供額外的功能或不同的功能。 Go的最新版本添加了其中幾個(gè)的容器包。 他們都有同樣的說明:他們處理interface{}值,這意味著你失去了所有類型的安全機(jī)制。
我們來看看sync.Map的一個(gè)例子,它是一個(gè)具有較低線程爭(zhēng)用的并發(fā)映射,而不是使用互斥鎖來保護(hù)常規(guī)映射:
- type MetricValue struct {
- Value float64
- Time time.Time
- }
- func main() {
- metric := MetricValue{
- Value: 1.0,
- Time: time.Now(),
- }
- // Store a value
- m0 := map[string]MetricValue{}
- m0["foo"] = metric
- m1 := sync.Map{}
- m1.Store("foo", metric) // not type-checked
- // Load a value and print its square
- foo0 := m0["foo"].Value // rely on zero-value hack if not present
- fmt.Printf("Foo square = %f\n", math.Pow(foo0, 2))
- foo1 := 0.0
- if x, ok := m1.Load("foo"); ok { // have to make sure it's present (not bad, actually)
- foo1 = x.(MetricValue).Value // cast interface{} value
- }
- fmt.Printf("Foo square = %f\n", math.Pow(foo1, 2))
- // Sum all elements
- sum0 := 0.0
- for _, v := range m0 { // built-in range iteration on map
- sum0 += v.Value
- }
- fmt.Printf("Sum = %f\n", sum0)
- sum1 := 0.0
- m1.Range(func(key, value interface{}) bool { // no 'range' for you! Provide a function
- sum1 += value.(MetricValue).Value // with untyped interface{} parameters
- return true // continue iteration
- })
- fmt.Printf("Sum = %f\n", sum1)
- }
這是個(gè)很好的例子來解釋為什么Go的生態(tài)系統(tǒng)中沒有太多的數(shù)據(jù)結(jié)構(gòu):與內(nèi)置的slice和map相比它們用起來很痛苦。出于一個(gè)簡(jiǎn)單的原因:Go的數(shù)據(jù)結(jié)構(gòu)中只有兩大類。
- aristocracy,內(nèi)置的slice,map,array和channel:類型安全,通用且調(diào)用range方便,
- Go代碼寫的其他的類型:不提供類型安全,因?yàn)樾枰獜?qiáng)制轉(zhuǎn)換所以用起來笨拙。
所以庫(kù)定義的數(shù)據(jù)結(jié)構(gòu)必須為我們開發(fā)者提供很多實(shí)在的好處,讓我們?cè)敢飧冻鍪ヮ愋桶踩皖~外冗長(zhǎng)代碼的代價(jià)。
當(dāng)我們想要編寫可重用的算法時(shí),內(nèi)置結(jié)構(gòu)和Go代碼之間的雙重性更加微妙。 這是標(biāo)準(zhǔn)庫(kù)的排序包對(duì)排序片段的一個(gè)例子:
- import "sort"
- type Person struct {
- Name string
- Age int
- }
- // ByAge implements sort.Interface for []Person based on the Age field.
- type ByAge []Person
- func (a ByAge) Len() int { return len(a) }
- func (a ByAge) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
- func (a ByAge) Less(i, j int) bool { return a[i].Age < a[j].Age }
- func SortPeople(people []Person) {
- sort.Sort(ByAge(people))
- }
等等…這是真的嗎? 我們必須定義一個(gè)新的ByAge類型,它必須實(shí)現(xiàn)3種方法來橋接泛型(“可重用”)排序算法和類型化片段。
對(duì)于我們開發(fā)者來說,唯一需要關(guān)注的一件事,就是用于比較兩個(gè)對(duì)象的Less函數(shù),并且它是域依賴的。其他一切都是干擾,因?yàn)镚o沒有泛型所以出現(xiàn)了模板。我們不得不一次次地重復(fù)使用它,包括我們想去排序的每個(gè)type和comparator。
更新:Michael Stapelberg指導(dǎo)我去看被我遺漏的sort.Slice。它在底層使用了反射,而且要求排序的時(shí)候,在slice上comparator函數(shù)得形成一個(gè)閉包。雖然這看起來會(huì)好些,但它依舊丑陋。
對(duì)于Go不需要泛型的所有解釋都是在告訴我們這就是“Go方式”,Go允許有可復(fù)用的算法來避免向下轉(zhuǎn)型成interface{}…
好了,現(xiàn)在來緩解一下痛苦,如果Go能用宏來生成這些無意義的模板將會(huì)變得美好一些,對(duì)嗎?
go generate:還行,但是…
Go 1.4引入了 go generate command命令來觸發(fā)源代碼中注釋的代碼生成。 那么,這里的“注釋”實(shí)際上意味著一個(gè)神奇的// go:generate,用嚴(yán)格的規(guī)則生成注釋:“注釋必須從行的開始處開始并且在//和go:generate之間沒有空格”。 弄錯(cuò)了,增加一個(gè)空格,沒有空格工具會(huì)警告你。
這實(shí)際上涵蓋了兩種用例:
- 從其他來源生成Go代碼:ProtoBuf / Thrift / Swagger模式,語言文法等
- 生成補(bǔ)充現(xiàn)有代碼的Go代碼,例如作為示例給出的stringer,它為一系列類型常量生成一個(gè)String()方法。
第一個(gè)用例是可以正常使用的,附加的是你不必使用Makefiles,生成指令可以接近生成的代碼的用法。
對(duì)于第二種用例,許多語言(如Scala和Rust)都有宏(在設(shè)計(jì)文檔中提到)可在編譯期間訪問源代碼的AST。 Stringer實(shí)際上導(dǎo)入了Go編譯器的解析器來遍歷AST。 Java沒有宏,但注釋處理器扮演著相同的角色。
許多語言也不支持宏,因此除了這種脆弱的注釋驅(qū)動(dòng)語法之外,沒有任何根本性的錯(cuò)誤,除了這種脆弱的注釋驅(qū)動(dòng)的語法之外,它看起來像是一種快速破解,它不知道怎么做了這個(gè)工作,而不是被認(rèn)真考慮為連貫的語言設(shè)計(jì)。
哦,你知道Go編譯器實(shí)際上有許多注釋/雜注和條件編譯使用這種脆弱的注釋語法?
結(jié)論
正如你可能猜到的那樣,我與Go有著或愛或恨的關(guān)系。 Go有點(diǎn)像一個(gè)朋友,你喜歡和他在一起,因?yàn)樗苡腥ぃ苓m合一起喝啤酒閑談,但是當(dāng)你想要進(jìn)行更深入的對(duì)話時(shí),你會(huì)覺得無聊或痛苦,而且你不想與他去一起度假
我喜歡Go編寫高效的API及網(wǎng)絡(luò)工具的簡(jiǎn)單性,這歸功于Goroutine,我討厭它在我必須實(shí)現(xiàn)業(yè)務(wù)邏輯時(shí)限制我的表現(xiàn)力,并且我討厭它的所有怪異和陷阱等著你踩進(jìn)去。
直到最近,Go還沒有真正的替代品,它正在開發(fā)高效的本地可執(zhí)行文件,而不會(huì)產(chǎn)生C或C ++的痛苦。Rust正在迅速發(fā)展,我越玩越多,我發(fā)現(xiàn)它越來越有趣和設(shè)計(jì)得非常好。我有一種感覺,Rust是需要一段時(shí)間才能相處的朋友之一,但是你最終會(huì)想要與他們建立長(zhǎng)期合作關(guān)系。
回到更技術(shù)的層面,你會(huì)發(fā)現(xiàn)文章中說的Rust和Go并不是一個(gè)層面的,由于Rust沒有GC等原因,Rust是一個(gè)系統(tǒng)語言。我認(rèn)為這越來越不符合實(shí)際。Rust在大型web框架和優(yōu)秀的ORM中的地位正在逐漸升高。它也給你一種親切感:“如果它是編譯器,錯(cuò)誤會(huì)出現(xiàn)在我寫的邏輯上,而不是我忘記注意的語言特性上”。
我們也從容器/服務(wù)網(wǎng)格領(lǐng)域上看到一些有趣的活動(dòng),包括使用Rust寫的高效Sozu代理,或者Buoyant(Likerd的開發(fā)者)開發(fā)的他們的新Kubernetes服務(wù)網(wǎng)格Conduit來作為Go和Rust的結(jié)合,其中Go作為控制層(我猜由于現(xiàn)有的 Kubernetes 庫(kù)),Rust作為數(shù)據(jù)層因?yàn)樗母咝Ш徒选?/p>
Swift也是可以替代C和C++語言的家族的一部分。盡管它的生態(tài)仍然太以Apple為中心,但是現(xiàn)在它在Linux是可以用的,而且出現(xiàn)了服務(wù)端API和Netty框架。
現(xiàn)在當(dāng)然沒有萬能和完全通用的技術(shù)。但是了解你使用的這些工具的缺點(diǎn)是很重要的。我希望這個(gè)博客已經(jīng)讓你了解到了一些關(guān)于Go的你曾經(jīng)沒意識(shí)到的問題,這樣你就可以避免陷阱而不會(huì)被陷進(jìn)去!