本文章翻譯自 https://blog.codeship.com/comparing-elixir-go
譯者:關于這篇譯文
編程語言的爭論是程序員的“圣戰(zhàn)”之一碴卧。另外幾個是:Tab vs. 空格, Vim vs. Emacs
Golang (以下稱作"Go") 和 Elixir 是兩種在很多方面有競爭關系的語言,應用場景類似机断,并且都比較年輕[1]召噩,所以比較這兩者儡湾,還是有意義的咐容。
我使用 Go 和 Elixir 都有一段時間了揉阎,有用他們來做過一些基礎的網(wǎng)絡應用程序[2]庄撮。我在學習、使用兩者的過程中毙籽,也一直在思考洞斯、探索兩者之間的區(qū)別,所以翻譯相關的文章的時候坑赡,應該不至于產(chǎn)生大的理解偏差烙如。
Go 是中國區(qū)程序員的新寵,大概 2015年底[3]毅否,突然在中國獲得了廣大程序員和(創(chuàng)業(yè))老板的青睞亚铁,我當時剛好有每天刷招聘網(wǎng)站的習慣螟加,所以對Go在中國的一夜爆發(fā)印象很深。而 Elixir 是個相對小眾的語言然爆,底層基于Erlang/OTP, 從Ruby社區(qū)吸取了很多優(yōu)點設計成的一個現(xiàn)代編程語言,雖然聽上去只是某一小撮兒程序員的玩具黍图,但實際上卻已經(jīng)非常成熟,完全可以用于生產(chǎn)環(huán)境剖张。
現(xiàn)實世界里到處都是權衡取舍,權衡自然要比較揩环。但比較計算機技術耗時費力:要比較他們,就必須對兩者都有一定的了解肯污。我們需要一些時間來研究書籍和文檔蹦渣,多數(shù)還要實地演練一下,才能對某種技術有一個比較全面的了解认臊。最后才能深刻理解其長處失晴、短處拘央、適應場景等灰伟。這是個過程栏账,要求程序員有鉆研的耐心挡爵,也要求老板開明竖般,給我們時間和自由去學習涣雕、嘗試各種新技術胞谭。
而比較編程語言更復雜一些丈屹。一是從初學習一個編程語言旺垒,到順利上手先蒋,再到對其擁有自己的看法需要更多的時間宛渐;二是編程語言容易形成心理壁壘,一旦掌握了某種編程語言鳞仙,很多程序員傾向于用它來解決任何問題[4]棍好,甚至激烈的捍衛(wèi)所謂的“世界上最好的語言”[5]借笙, 這影響了我們的理性判斷业稼。我自認在翻譯此文章的時候盼忌,也無法做到完全隱藏個人偏好傾向,但盡量保持中立君编,少做誘導性陳述吃嘿。
編程語言在程序員找工作的過程中兑燥,也發(fā)揮著特殊的作用琴拧◎拘兀“編程語言不重要” 之類的話沒有實際意義沛膳,因為目前的企業(yè)招聘锹安,往往是基于編程語言來尋找候選人的,比如瞄桨,企業(yè)會指定招聘 C/C++ 程序員芯侥,或 Python 程序員柱查。企業(yè)有自己實際情況的考慮唉工,不同編程語言的應用場景不一淋硝,e.g. 招聘 OC / Swift 語言程序員其實就是招聘iOS平臺開發(fā)谣膳。企業(yè)里前端继谚、后端花履、移動端各團隊的細致分工诡壁,讓多數(shù)前端很少碰后端的知識妹卿,后端的也很少接觸前端的內容纽帖,這種分化日趨嚴重懊直。但事情本不該如此室囊,程序員本就是獨立工作者融撞,與寫作尝偎,音樂創(chuàng)作類似致扯,程序員的世界里個人的影響要大于群體抖僵,所謂的群策群力耍群、團隊精神是一種誤導畏线,看看 github 上的項目耘婚,多數(shù)貢獻都來自一兩個核心開發(fā)者沐祷,其他零星的 bug 修復雖然數(shù)量眾多卻不能影響項目的發(fā)展赖临。未來兢榨,程序員建立個人影響力可能會越來越重要吵聪。上班族會逐漸變?yōu)樽杂陕殬I(yè)者吟逝,為公司工作逐漸過渡到為自己工作块攒,那時候對各種技術都有廣泛涉獵的程序員囱井,將更有競爭力庞呕,而目前企業(yè)對程序員技術棧的細致分工住练,顯然是對其個人能力的妨礙澎羞。
要特別聲明的是, 僅通過比較兩種編程語言的熱度妆绞,不能說明某種編程語言更“好”株茶、更值得學習启盛、更有前景僵闯。一種語言“流行”的原因很復雜鳖粟,但跟語言設計是否精良應該沒有太大關系向图。使用某種語言的程序員的平均薪資水平榄攀,與其熱度也不吻合檩赢,市場價格取決于供需漠畜,跟語言沒有太大關系蝴悉。抱著闡述語言“前景”的目的寫文章也沒有意義拍冠,因為不論最后結論怎樣庆杜,都無法改變讀者現(xiàn)有的愛好傾向晃财,只能將其加深:文章見解與你的相符則贊之并深以為然断盛,相違則嗤之以鼻钢猛。其實最有用的建議是兩種都學。掌握多種編程語言已經(jīng)成為邁向優(yōu)秀程序員必須的一步壶愤。每個編程語言的背后是一個大的社區(qū)生態(tài)環(huán)境公你,掌握了語言本身,才能了解這個生態(tài)環(huán)境中的技術脱茉。
文章原文及本譯文從來沒有打算說服你學習某種編程語言,排斥另外一種榜田。
比較的意義在于箭券,一者了解文章中提到的未聽說過的技術概念辩块,擴展視野废亭;二來通過這種比較豆村,我們能夠了解每種語言在設計上所作出的權衡取舍抵碟。編程語言作為一個設計給其他程序員使用的產(chǎn)品拟逮,向前兼容是強制性的:一旦第一版發(fā)布敦迄,其中的設計錯誤有可能再也沒有機會修改罚屋。而正是這些設計決策,決定了某種語言更適合解決什么問題猛拴,了解這些愉昆,幫助我們在面對特定問題的時候跛溉,做編程語言層面的抉擇专肪。
之前一直想寫類似的文章嚎尤,遲遲未能動筆诺苹。前幾天偶然搜到這一篇文章收奔,感覺作者的理解很到位,覆蓋很全面翩肌,比我自己寫肯定好多了念祭,所以決定直接翻譯過來給大家分享。搬運過來站玄,再加上自己對某些概念的注解株旷,就是一篇很好的技術文章了。所以讀的時候钞瀑,千萬不要漏掉了注解的內容。
又要睡覺了显晶, 還是一個字都沒翻譯偿警!前面好像已經(jīng)坑了四個人螟蒸,對不住只能再坑一下已經(jīng)讀到這里的朋友。诵原。
2018年02月03日19:29:34 開工, again
搬運正文
在過去的幾年里, Elixir 和 Go 語言的熱度都有顯著的增長, 并且都經(jīng)常被正在尋找高并發(fā)解決方案的開發(fā)人員所嘗試. 這兩種編程語言遵循著相似的原則, 但都做出了一些核心的, 影響了他們可能的應用場景的權衡取舍.
讓我們通過了解他們的背景, 編程風格, 以及怎樣處理并發(fā)等方面, 比較這兩者.
太懶了,2018年02月08日13:28:57 開工, again again
背景
Go 語言 是 2009 年由Google開發(fā)的吗蚌,編譯后是平臺相關的本地可執(zhí)行文件[6]. 開始的時候,它是作為一個實驗性的項目侮措,目的是針對其他編程語言[7] 的主要槽點分扎,并保留他們的強項,開發(fā)出一個新的編程語言菲饼。
Go 的目標設定是,平衡開發(fā)速度饼煞,并發(fā)砖瞧,性能息堂,穩(wěn)定,可移植性 和 可維護性块促,而它也完美的達成了這一目標荣堰。所以呢,Docker 和 InfluxDB 就是用 Go 開發(fā)的竭翠,并且 很多主流公司 (包括 Google, Netflix, Uber, Dropbox, SendGrid 和 SoundCloud) 也在使用 Go 開發(fā)各式各樣的工具振坚。
Elixir 語言 是 2011 年由來自 Plataformatec 的 Jose Valim 開發(fā)的逃片。它運行在 Erlang BEAM 虛擬機之上屡拨。
Erlang 最早是 1986 年,由愛立信公司為高可用褥实,分布式的電話系統(tǒng)開發(fā)的呀狼。此后 Erlang 被拓展到了許多其他領域,比如 web server, 并達到過 9 個 9 的可用性 (31 毫秒/年 的宕機時間).
Elixir 的設計目標是损离,讓 Erlang VM 有更高的可擴展性哥艇,更高的生產(chǎn)力,同時保持跟 Erlang 生態(tài)圈的兼容性僻澎。為完成這一目標貌踏,它允許在 Elixir 代碼中使用 Erlang 庫,反之亦然 (Shawn: Erlang 代碼中也可以使用 Elixir 庫)窟勃。
為避免重復, 在本文中祖乳,我們把 "Elixir/Erlang/BEAM" 都統(tǒng)稱為 "Elixir"
許多公司 在生產(chǎn)環(huán)境中使用 Elixir。包括 Discord, Bleacher Report, Teachers Pay Teachers, Puppet Labs, Seneca Systems, 和 FarmBot秉氧。很多其他的項目是用 Erlang 構建的眷昆,包括 WhatsApp, Facebook 的聊天服務, 亞馬遜的 CloudFront CDN, Incapsula, Heroku’s 路由和日志層, CouchDB, Riak, RabbitMQ, 還有占世界將近一半的電話系統(tǒng)。
編程風格
要對 Elixir 和 Go 作出一個可靠的比較汁咏,就要理解他們各自的 run-time [8] 的核心原則亚斋。因為這些基礎構件是其他一切的來源。
對于有傳統(tǒng)的 C 風格編程背景的人來說攘滩,Go 的風格看起來會更熟悉一點帅刊,即使這個語言做了一些更偏愛函數(shù)式編程風格的決定。你將看到你感覺很熟悉的 靜態(tài)類型漂问,指針赖瞒,還有結構體女揭。
函數(shù)可以從結構體創(chuàng)建,也可以附著于結構體上冒黑,但以一種更加可組合的方式田绑。函數(shù)可以在任何地方創(chuàng)建勤哗,并附著到類型上抡爹,而不是把函數(shù)嵌入到對象里,然后再從那個對象擴展(繼承).
如果需要使用多個結構體類型調用一個方法芒划,可以定義個接口冬竟,來提供更好的靈活性。跟通常的面向對象的語言里的接口不大一樣民逼,那些語言里泵殴,必須在一開始定義某個對象實現(xiàn)了某個接口。在 Go 語言里拼苍,接口可以自動的應用在一切匹配得上的類型上面笑诅。這里有一個 Go 接口的好例子。
Elixir 更偏愛函數(shù)式風格疮鲫,但摻雜了一些來自面向對象編程的原則吆你,讓它看起來沒那么另類,讓來自面向對象領域的程序員轉到函數(shù)式風格時更容易些俊犯。
Elixir 里變量是不可變的妇多,并且使用消息傳遞(來共享數(shù)據(jù)),所以不會把指針傳來傳去燕侠。這意味著函數(shù)操作在函數(shù)式編程里面更加“字面化”:傳進一些參數(shù)去者祖,然后返回一個沒有副作用的結果。這簡化了開發(fā)中的很多方面绢彤,包括測試 和 代碼可讀性七问。
由于數(shù)據(jù)不可變,所以像 for 循環(huán)之類的常見的操作茫舶,在 Elixir 里面沒法用械巡,因為你沒法兒遞增一個計數(shù)器(counter)[9]。這種的操作要用遞歸實現(xiàn)奇适,盡管 Enum
庫以一種很方便的形式坟比,提供了一些常見的循環(huán)模板。
因為遞歸的使用是如此頻繁嚷往,Elixir 也使用 尾遞歸: 如果函數(shù)的最后一個調用是調用它本身的話葛账,調用棧不會增長,避免了棧溢出的錯誤[10]皮仁。
Elixir 到處都在使用模式匹配籍琳,有點像 Go 使用 接口 的那種方法菲宴。在 Elixir 里,一個函數(shù)可以這樣定義:
def example_pattern(%{ "data" => %{ "nifty" => "bob", "other_thing" => other}}) do
IO.puts(other)
end
通過使用 map 模板作為參數(shù)趋急,僅僅當傳進來的參數(shù)滿足以下條件時喝峦,這個函數(shù)才會被調用:參數(shù)是一個 map 類型,并且有個 "data"
字段呜达,"data"
字段的值又是一個嵌套的 map 類型谣蠢,它里面包含了一個值為 "bob"
的 "nifty"
字段,以及一個 "other_thing"
字段查近。
匹配成功的話眉踱,參數(shù)的 "other_thing"
字段的值會被賦值給 other
變量,在函數(shù)體內就可以訪問other
變量了霜威。
模式匹配被用在各種地方谈喳,從函數(shù)參數(shù)到變量賦值,特別是遞歸戈泼。這里有幾個例子來幫助理解模式匹配婿禽。結構體可以被定義為類型,然后一樣被用在模式匹配里大猛。
這些方式(Go 的 Interface 和 Elixir 的模式匹配)在原則上很相似扭倾,都將數(shù)據(jù)結構和數(shù)據(jù)操作分離開來,都用匹配的方式來定義函數(shù)調用胎署,Go 是通過接口吆录,而 Elixir 通過模式匹配。[11]
雖然 Go 允許通過特定的類型來調用一個函數(shù)琼牧,比方說 g.area()
, 但跟 area(g)
基本是一回事兒恢筝。這方面,兩種語言唯一的區(qū)別是巨坊,在 Elixir 里撬槽,area()
必須返回一個結果,但 Go 里 area()
可能潛在的操作了一個內存中的引用趾撵。[12]
由于這種方式侄柔,兩種語言都有很強的組合型,意思是說占调,在一個項目的生命周期中暂题,不用去操作、擴展究珊、插入龐大的繼承樹薪者。這對于長時間運作的大項目來說,是個大福音剿涮。
這里最大的區(qū)別是言津,Go 語言里攻人,模板[13] 被定義在函數(shù)之外,用來復用悬槽,但如果沒有組織好的話怀吻,可能會導致很多重復的接口。而 Elixir 沒有辦法這么容易的復用模板初婆,但這樣模板就一直定義在它使用的地方蓬坡,不會亂。[14]
Elixir 是強類型的烟逊,但不是靜態(tài)類型的渣窜。大多數(shù)時候,是通過類型隱式推斷來判斷類型的宪躯。Elixir 里面沒有運算符重載,當你想用 +
來串聯(lián)兩個字符串的時候位迂,一開始就會比較困惑访雪,因為在 Elixir 里,應該用 <>
掂林。
如果你不理解背后的緣由的話臣缀,這種設定看起來好沒勁。編譯器能用明確指定的運算符來推斷 +
的兩邊都是數(shù)字泻帮,類似的精置,<>
兩邊都必須是字符串。
強類型基本上意味著锣杂,通過使用 Dialyzer脂倦,除了一些在模式匹配里有歧義的參數(shù)之外,編譯器幾乎可以捕獲所有類型元莫。在這些例外情況下赖阻,可以使用代碼注釋來定義變量類型。這樣做的好處是踱蠢,在不丟失動態(tài)類型的靈活性的和元編程的特權的同時火欧,享受靜態(tài)類型的大多數(shù)好處。
Elixir 文件可以用 .ex 擴展名表示需要編譯的代碼文件茎截,.exs 擴展名表示邊編譯邊執(zhí)行的腳本文件(就像 shell 腳本等一樣)苇侵。Go 是一直需要編譯的,但是 Go 的編譯器太快了企锌,即使編譯巨大的代碼庫榆浓,感覺也是瞬間完成。
并發(fā)
并發(fā)是比較的重點霎俩,現(xiàn)在你已經(jīng)對編程風格有了一個基本輪廓的了解哀军,所以接下來將更容易理解一些沉眶。
傳統(tǒng)上,并發(fā)是通過線程來實現(xiàn)的杉适,但線程比較“重”谎倔。最近,各種編程語言開始使用“輕量的線程”或者叫“綠色的線程”猿推,并使用存在于單線程里的調度器 輪流調度不同的邏輯片习。
這種并發(fā)模型讓內存使用更加高效,但這依賴 runtime 來規(guī)定調度的流程蹬叭。JavaScript 已經(jīng)在瀏覽器里使用這種模式很多年了藕咏。一個簡單的例子:當你聽到 JavaScript 的 “非阻塞式 I/O” 時,就意味著秽五,那個線程里執(zhí)行的代碼孽查,會在 I/O 開始時,把控制權讓渡給調度器坦喘,以讓調度器做一些其他的事情盲再。
合作式 vs. 搶占式 調度
Elixir 和 Go 都使用調度器來實現(xiàn)各自的并發(fā)模型。雖然這兩種語言天生都會將任務均分到多處理器上瓣铣,但 JavaScript 不行答朋。
Elixir 和 Go 是用不同的方法實現(xiàn)并發(fā)的。Go 使用的是合作式的調度棠笑,這意味著梦碗,當前正在運行的代碼必須主動讓出控制權,另一個操作才能獲得被調度的機會蓖救。而 Elixir 使用的是搶占式調度洪规,每個進程都會被預置一個執(zhí)行窗口,這是被強制保證的藻糖,任何情況下都如此淹冰。
在性能方面的表現(xiàn)上,合作式調度更高效巨柒,因為搶占式調度在強制保證執(zhí)行窗口時樱拴,產(chǎn)生了額外的執(zhí)行損耗。搶占式調度更穩(wěn)定(Shawn: 是說表現(xiàn)穩(wěn)定洋满,不是工作穩(wěn)定晶乔。比如,在吞吐量上牺勾,搶占式調度表現(xiàn)更穩(wěn))正罢,意思是說,不會因為一個大而耗時的操作一直不釋放控制權驻民,而推遲數(shù)以百萬級的小操作的調度翻具。
作為預防措施履怯,Go 程序員可以通過在代碼里插入 runtime.Gosched()
來更頻繁的(主動)歸還CPU控制權, 來應對這種潛在的代碼問題。(Elixir 的)運行時級別的保證讓第三方庫和軟實時系統(tǒng)的可信度更高裆泳。
Goroutine vs. 輕量進程
為了在 Go 里執(zhí)行并發(fā)操作叹洲,我們用 Goroutine,就是簡單的在一個方法前面加上一個 go
關鍵字工禾,任何方法都行运提。所以這樣:
hello("Bob")
// To...
go hello("Bob")
這方面 Elixir 與之差不多,它不用 Goroutine闻葵,但你可以 spawn
一個 進程(澄清一下:不是操作系統(tǒng)的那種進程民泵,以后所說的進程都是輕量進程)出來。
# From...
HelloModule.hello("Bob")
# To...
spawn(HelloModule, :hello, ["Bob"])
# Or by passing a function
spawn fn -> HelloModule.hello("Bob") end
這里主要的不同是槽畔,go
操作什么都沒有返回栈妆,而 spawn
返回了新創(chuàng)建的進程的 id.
兩個體系的進程間通信風格相似,都使用“消息隊列”來通信竟痰。Go 語言里稱其為 “ channel”签钩,Elixir 里叫它 “進程信箱”。
在 Go 語言里可以定義一個 channel坏快,只要有這個 channel 的引用,任何進程都可以給 channel 發(fā)消息憎夷。在 Elixir 里莽鸿,可以通過“進程ID”或者“進程名”來給進程發(fā)消息。Go 的 channel 在定義的時候給消息指定了數(shù)據(jù)類型拾给,而 Elixir 的進程信箱用的是模式匹配祥得。
給一個 Elixir 進程發(fā)消息,等同于給一個 Goroutine 監(jiān)控下的 channel 發(fā)送消息蒋得。這里給出個例子:
go channel:
messages := make(chan string) // Define a channel that accepts strings
go func() { messages <- "ping" }() // Send to messages
msg := <-messages // Listen for new messages
fmt.Println(msg)
send self(), {:hello, "world"}
receive do
{:hello, msg} -> msg # This reciever will match the pattern
{:world, msg} -> "won't match"
end
另外级及,兩者都有辦法設置接收消息的超時時間。
由于 Go 是允許內存共享的额衙,Goroutine 也可以直接改變一個內存中的 (channel) 引用饮焦,雖然這時必須使用互斥鎖來防止競態(tài)條件。比較理想方式是窍侧, 一個 channel 上县踢,應當只有一個 goroutine 監(jiān)聽并更新其共享的內存,以避免互斥鎖的需求伟件。
在這個(基礎)功能之外硼啤,好戲才剛剛開始。
Erlang OTP 提供了一系列 使用并發(fā)和分布式的 “最佳做法” 的模板(叫設計模式也行)斧账。在 Elixir 里谴返,多數(shù)情況下你不會直接去碰 spawn
, send/receive
這些基礎函數(shù)煞肾,一般我們都用這些抽象提供給我們的功能。
這些封裝包括 Task
嗓袱,用來做簡單的 async/await
風格的調用籍救。Agent
用來使用并發(fā)的進程來維護和更新共享的狀態(tài)。GenServer
用來做更復雜的邏輯索抓。等等钧忽。
為了限制一個隊列上的并發(fā)數(shù),Go 的 channel 實現(xiàn)了接收指定數(shù)量消息的 buffer (達到上限時逼肯,阻塞發(fā)送者)耸黑。默認情況下,在有進程準備好接收消息之前篮幢,channel 一直被阻塞大刊,除非設置了 buffer。
Elixir 的信箱默認是不限制消息數(shù)量的三椿,但可以使用 Task.async_stream
來定義一個操作的并發(fā)數(shù)缺菌,然后跟 Go channel 一樣的阻塞發(fā)送者。
進程在 Elixir 和 Go 里都很輕量搜锰,一個 goroutine 差不多 2KB伴郁,而一個 elixir 進程差不多 0.5KB。Elixir 進程有自己的獨立內存蛋叼,在進程結束時自動回收焊傅,而 goroutine 使用共享內存,資源回收要使用應用程序級別的垃圾回收狈涮。
錯誤處理
這可能是兩者間的一個最大的區(qū)別狐胎。Go 語言在各個層級非常顯示的做代碼處理,從函數(shù)調用到 panic. 但在 Elixir 里歌馍,錯誤處理被當做一種 "code smell" [15]握巢。我等你一小會兒,你再把這句話讀一遍松却。
那么這是怎么做到的呢暴浦?還記得剛才我們談到,Elixir 的 spawn
返回了一個 進程 ID 吧玻褪?它可不僅僅是用來發(fā)送消息的肉渴。它也可以被用來監(jiān)視那個進程,并且檢查它是否還活著带射。
因為進程太輕了同规,所以 elixir 里標準的的做法是創(chuàng)建倆。一個就是那個進程,另一個用來監(jiān)控這個進程券勺。這種方式叫做 supervisor 監(jiān)控模式绪钥。Elixir 應用傾向于工作在監(jiān)控樹之下。監(jiān)控者在后臺使用另外一個方法 spawn_link
來創(chuàng)建進程关炼,如果進程崩潰了程腹,監(jiān)控者就去重啟它。
除0 的運算把進程搞崩了儒拂,supervisor 馬上重啟它寸潦,讓后面的操作得以正常執(zhí)行。用 supervisor 就簡單直接的實現(xiàn)了這個功能社痛。
相反的见转,Go 沒有辦法跟蹤單個 Goroutine 個體的執(zhí)行[16]。錯誤處理必須在各個層次顯示的進行蒜哀,導致了許多這種的代碼:
b, err := base64.URLEncoding.DecodeString(cookie)
if err != nil {
// Handle error
}
這里我們指望著在錯誤可能發(fā)生的地方都有錯誤處理斩箫,不論是不是在 goroutine 里面。
Goroutine 可能會將錯誤情形用同樣的方式傳給 channel撵儿。然而乘客,如果 panic 發(fā)生了,每一個 Goroutine 都要保證滿足自己的恢復條件淀歇,要不然整個應用程序都會崩易核。Panic 不等同于其他語言里的“異常”浪默,因為 Panic 基本上都是系統(tǒng)級的 “stop everything” 的事件[17]耸成。
內存溢出錯誤可以完美的概括這種情形,如果一個 goroutine 觸發(fā)了一個內存溢出的錯誤浴鸿,即使有著適當?shù)腻e誤處理,整個 Go 程序都會崩弦追,因為內存狀態(tài)是共享的岳链。
Elixir 里,因為每個進程都有自己的堆空間劲件,可以對每個進程設置堆大小掸哑,達到這個限制的話會掛掉這個進程,但然后零远,Elixir 會獨立的垃圾回收那個進程的內存苗分,然后重啟它,而不會影響其他任何東西牵辣。
這不是說 Elixir 刀槍不入摔癣,VM 本身也可能會因為其他的問題內存溢出,但在進程里的話,這是個可控的問題择浊。
這也不是在批評 Go戴卜,這是一個幾乎所有的、使用共享內存的語言的通病 -- 意思是說琢岩,幾乎每一個投剥、你聽說過的語言。這是 Erlang/Elixir 設計上的一個強項担孔。
Go 的策略迫使在每一個可能出錯的位置處理錯誤江锨,這需要一個清晰的設計思想,并且可能促使一個經(jīng)過深思熟慮的應用的誕生糕篇。
如 Joe Armstrong 所說啄育,關鍵點在于,Elixir 模式是娩缰,你可以預期其永遠不會停的應用程序灸撰。你也可以通過 suture library 手動實現(xiàn)一個 Go 版的 supervisor唆垃,因為你可以手動調用 Go 的調度器迁沫。
注意:在大多數(shù) Go 的 Web server 里,panics 在 handlers 里已經(jīng)被解決了断医。所以泰鸡,一個 web 請求還沒有嚴重到足以讓整個應用掛掉的程度债蓝,不會的。當然在你自己的 Goroutine 里面還是得自己處理 panic盛龄。不要讓我的陳述暗示 Go 很脆弱饰迹,因為它不脆弱。
可變 vs. 不可變
對于比較 Elixir 和 Go 來說余舶,理解 數(shù)據(jù)可變 vs. 不可變 之間的權衡很重要啊鸭。
Go 使用了多數(shù)程序員熟悉的內存管理風格:有共享內存,指針匿值,可以被改變和重新賦值的數(shù)據(jù)結構赠制。對于傳遞大的數(shù)據(jù)結構來說,這將會高效很多挟憔。
不可變的數(shù)據(jù)利用了 Copy-On-Write 技術钟些。意思是說,在相同的棸硖罚空間里(Shawn: 同一個進程里)政恍,數(shù)據(jù)傳遞其實只是傳了指向數(shù)據(jù)的指針而已,只有想改動那個數(shù)據(jù)的時候达传,底層才 Copy 一個副本出來讓你改篙耗。
舉個例子來說迫筑,一個由很多數(shù)據(jù)組成的列表,其實可能只是一個包含了 指向那些不可變數(shù)據(jù)的指針 的列表而已鹤树。而對這個列表排序铣焊,可能只是返回了一個順序不同的指針列表,因為那些被指向的數(shù)據(jù)本身可以認定是不可變的罕伯。改變列表中的某個數(shù)據(jù)的值曲伊,將得到一個新的指針列表,其中包含了剛才被修改的那個數(shù)據(jù)的指針 (當然是個新地址了追他,原數(shù)據(jù)沒動)坟募。但是當我把這個列表傳遞給另外一個進程的時候,整個列表包括其中的值都會被 Copy 到新的椧乩辏空間懈糯。
集群
由可變數(shù)據(jù) vs. 不可變數(shù)據(jù)引出的另外一個問題是集群。
使用 Go 語言单雾,只要你想赚哗,你是有辦法實現(xiàn)一個無縫的 RPC 的。但是由于指針和共享內存硅堆,如果你調用一個遠程方法屿储,并傳了一個指向本地資源的引用的話,它不會正常工作的[18]渐逃。
在 Elixir 里面够掠,所有的操作都是通過消息傳遞的。整個應用程序可以做成任意數(shù)量節(jié)點的集群茄菊。數(shù)據(jù)被傳給一個函數(shù)疯潭,而這個函數(shù)返回一個新值。任何函數(shù)的調用都不會引起內存內的數(shù)據(jù)改動面殖,這允許 Elixir 在調用不同検ǎ空間里的函數(shù)、不同機器上的函數(shù) 或不同數(shù)據(jù)中心里的函數(shù)時脊僚,跟調用本椘诜幔空間里的函數(shù)的方式一模一樣。
很多應用程序是不需要集群的吃挑,但是也有很多應用因為集群受益匪淺。比如聊天程序街立,用戶可能連接到了不同的服務器上舶衬,他們之間需要通信∈昀耄或者水平分布的數(shù)據(jù)庫逛犹。這兩種場景是 Phoenix 框架和 Erlang Mnesia 的常見應用案例。對于那些不使用產(chǎn)生瓶頸的中繼節(jié)點的應用程序來說,集群是他們水平伸縮能力的關鍵虽画。
工具庫
Go 有個 可擴展的標準庫舞蔽,讓大多數(shù)開發(fā)者不用去找第三方庫,幾乎就能做任何事情码撰。
Elixir 標準庫 更精簡一些渗柿,但它包含了更全的 Erlang 標準庫 ,Erlang 標準庫包含了三個預置的數(shù)據(jù)庫 ETS/DETS/Mnesia脖岛,其他的軟件包必須從第三方庫拉取朵栖。
Elixir 和 Go 都有無數(shù)的第三方庫可以用,Go 直接使用 go get
來從遠程導入軟件包柴梆,而 Elixir 使用 Mix陨溅。Mix 是一個編譯工具,它用一種大多數(shù)編程語言的用戶習慣的方式绍在,調用 Hex 包管理工具门扇。
Go 仍然在努力統(tǒng)一 整個語言范圍內的包管理解決方案。在包管理解決方案 和 可擴展的標準庫之間偿渡,只要能用標準庫臼寄,多數(shù) Go 社區(qū)的人似乎還是喜愛用標準庫。已經(jīng)有很多 包管理工具 可以用了卸察。
部署
Go 的部署簡單明了脯厨。一個 Go 程序可以編譯成一個單獨的可執(zhí)行文件,所有的依賴都包含在里面坑质。然后可以本地化的執(zhí)行合武。跨平臺的涡扼,各種地方都能跑 (編譯的時候預先指定目的機器的架構類型)稼跳。Go 的編譯器可以為任何目標架構編譯可執(zhí)行文件,而不管你的當前運行 Go build 的機器是什么架構的吃沪。這是 Go 的大強項汤善。
Elixir 其實有很多部署方式,但最主要的方式還是通過優(yōu)秀的 distillery 工具來部署票彪。它把你的 Elixir 應用以及所有依賴打包成一個可執(zhí)行文件红淡,然后可以部署到目標機器上。
兩種語言的最大區(qū)別是降铸,Elixir 編譯環(huán)境的 架構類型 必須跟 目標機器的架構類型一樣在旱。官方文檔里有很多關于這種情形的變通解決方案,但最簡單的一種推掸,是在一個跟目標機器相同的 Docker 容器環(huán)境里編譯發(fā)布包桶蝎。
有了上述兩種方式驻仅,你可以直接停掉當前運行的代碼,然后替換掉可執(zhí)行文件登渣,然后重啟噪服,就像多數(shù)今天的 Blue-Green [19] 部署方式那樣。
熱更新
Elixir 還有 BEAM 帶來的另外一種部署方法胜茧。這東西有點復雜粘优,但對于某些特定類型的應用,好處非常大竹揍。叫做 “熱加載” 或者 “熱升級”敬飒。
Distillery 工具基于這個功能出發(fā),簡化了這個過程芬位,你只需要在編譯發(fā)布版時无拗,在你的命令行里加上 --upgrade
標志。但這不代表任何情況下你都需要它昧碉。
在討論什么情況下使用它之前英染,你需要先理解它是做什么的。
Erlang 最開始是給電話系統(tǒng)(OTP 的意思是 Open Telecom Platform) 使用的被饿。目前這個星球上有近一半的電話系統(tǒng)是用 Erlang 的四康。Erlang 被設計為 “永不停止” 的語言。當有很多通話正在使用著你的系統(tǒng)的時候狭握,“永不停止” 在部署的時候是個很復雜的問題闪金。
如果不斷開所有人的連接,你怎么部署呢论颅?難道你需要先阻止所有新打進來的電話哎垦,然后禮貌的等待正在進行中的通話結束嗎?
答案是不恃疯。這就是“熱加載”的來由漏设。
由于 Erlang 進程之間的棧空間隔離今妄,升級可以不打斷現(xiàn)有的進程郑口。沒跑起來的進程可以被替換掉,新啟動的進程接收處理新的網(wǎng)絡數(shù)據(jù)盾鳞,并跟老進程一起并肩運行犬性。正在跑的老進程可以一直干活,直到他們的工作完成腾仅。
這允許你再數(shù)百萬通話正在進行的時候仔夺,部署升級。不打斷老的通話攒砖,讓他們自己結束缸兔。你可以想想一下你吹泡泡的時候,新的泡泡慢慢替代掉了天空中的老泡泡 ... 基本上熱更新也是這樣子的原理吹艇,老的泡泡仍然在飛來飛去惰蜜,直到破滅。
理解了這個東西受神,我們可以看一下熱更新比較合適的潛在應用場景:
- 聊天程序抛猖。每個用戶使用 WebSocket 連接到指定的機器上。
- 任務服務器鼻听。你希望在不影響正在運行的任務的同時做更新部署财著。
- CDN服務器。在一個很慢的網(wǎng)絡連接上撑碴,一個小的網(wǎng)絡請求后面跟著一大堆正在進行的轉發(fā)包撑教。
拿 WebSocket 來說,這允許你更新一個可能有著百萬活動連接的機器醉拓,而不會導致數(shù)以百萬計的重連請求來轟炸服務器伟姐,也不會丟失正在發(fā)送中的消息。順便說一句亿卤,這就是為啥 WhatsApp 是基于 Erlang 的愤兵。熱更新已經(jīng)被用于,在飛機飛行的過程中排吴,更新航天系統(tǒng)秆乳。
缺點是,如果你需要回滾的話钻哩,熱更新會更復雜屹堰。只有當你真的有場景需要它的時候,才應該用熱更新憋槐。但是你知道你有辦法做熱更新双藕,這挺好的。
集群也是一樣阳仔,你不一定總是需要它忧陪,但是一旦用到了,它就是必不可少的近范。集群和熱更新與分布式系統(tǒng)攜手同行嘶摊。
結論
這篇文章很長,但希望它給了你一個對 Go 和 Elixir 之間差異的真切的輪廓评矩。我發(fā)現(xiàn)一個最好的辦法理解他們的差異:把 Elixir 想象成一個操作系統(tǒng)叶堆,而把 Go 想象成特殊程序。
在設計一個 特別快速斥杜、目的相當明確的解決方案時虱颗,Go 表現(xiàn)非常優(yōu)異沥匈。Elixir 提供了一個環(huán)境,在其中很多不同的程序共存忘渔、運行高帖、互相交流 (哪怕是在部署更新的時候)。你將用 Go 構建單獨的微服務個體畦粮。你將在一個單獨的 Elixir umbrella 項目里構建很多個微服務散址。
學習 Go 語言相對明確、簡單些宣赔。Elixir 的話预麸,你一旦掌握了其竅門兒,就非常直白了儒将。但是如果你的目標是在使用之前全學完的話吏祸,浩瀚的 Erlang 和 OTP 的世界會挺嚇人的。
兩者都是在我的推薦清單里的優(yōu)秀的編程語言椅棺,幾乎可以做任何編程里的事情犁罩。
對于非常特定的代碼,可移植的系統(tǒng)層級的工具两疚,對性能敏感的任務床估、API, Go 很難被超越。對于 全棧的網(wǎng)絡應用诱渤,分布式系統(tǒng)丐巫,實時系統(tǒng),和 嵌入式應用勺美,我將會使用 Elixir递胧。
Finished! 2018年02月12日18:21:23
-
Golang 好像是在09年出來,我當時在讀大二赡茸,當時同學在對我們講其“令人震驚”的特性時缎脾,我是沒多大感觸的。Elixir 應該是 2014 才發(fā)布的占卧,我在學習Erlang的時候第一次被同事安利遗菠。 ?
-
比如,用 Golang 寫各種移動平臺的 Push Provider华蜒,從消息隊列里讀取消息辙纬,然后轉發(fā)到小米,華為叭喜,GCM, APNS贺拣。用 Elixir 寫 WebServer, 提供 RESTful API。 ?
-
當年也是 Docker 爆發(fā)的一年。 ?
-
所以 Node.JS 會大行其道! ?
-
是的就是PHP ... 這種傾向也很容易解釋:當我們秉持某種信念的時候譬涡,我們會自動的摒棄和忽略相反的觀點闪幽,而只接受相同的觀點。所以通過網(wǎng)絡搜索“xxx好不好”的問題涡匀,除了讓我們的執(zhí)念加深之外沟使,并無用處 -- 人性的弱點如此。 ?
-
Shawn:平臺指的是諸如 i386, x86_64 這種CPU架構渊跋,編譯 Go 程序時,可以指定目標操作系統(tǒng)的架構着倾。如 https://www.digitalocean.com/community/tutorials/how-to-build-go-executables-for-multiple-platforms-on-ubuntu-16-04 ?
-
Shawn:其實說的主要就是 C++拾酝。Go 的開發(fā)者之一 Ken Thompson 接受采訪時有一句話,說他們不喜歡 C++ (一方面原因是過于復雜)卡者。"Yes. When the three of us [Thompson, Rob Pike, and Robert Griesemer] got started, it was pure research. The three of us got together and decided that we hated C++." ?
-
Shawn: run-time 或者直接叫 runtime蒿囤。運行時系統(tǒng), 或者叫運行時環(huán)境。指的是那個提供諸如垃圾回收崇决,線程材诽、進程調度等基礎功能的系統(tǒng)。一般情況情況下恒傻,說 "Java 的 runtime " 就指的是 Java VM脸侥。而 Erlang 的 runtime 指的是 Erlang BEAM VM。Go 沒有虛擬機盈厘,但 Go 也有提供上述功能的 runtime. Go 的 runtime 在 Go 程序編譯時睁枕,編譯到那個可執(zhí)行文件里面了,有點類似于 C 的 libc沸手。 ?
-
Shawn: 意思就是說不支持像
for (int i = 0, i < 100; i ++) {// do something;}
這種的操作外遇,因為 "i" 是不能變的, i++ 這種操作是不允許的。 ? -
Shawn: 不大理解的話契吉,你可以去查查 “尾遞歸”跳仿。 ?
-
Shawn: 好像是有那么點意思,但 Elixir 的模式匹配顯然要強大多了捐晶。Go 的 interface 大概只能在實現(xiàn)多態(tài)調用的時候用菲语。而 Elixir 的模式匹配隨處可見,匹配函數(shù)租悄,匹配case分支谨究,強制賦值(let it crash!),各種地方都在用泣棋。 ?
-
Shawn: 我覺著這是 Go 語言設計時沒考慮清楚的地方胶哲。Go 語言的函數(shù)在使用上很有點函數(shù)式的樣子,但卻不支持不變的變量潭辈,函數(shù)調用也允許程序員隨意的更改共享的全局變量鸯屿。這種允許會慫恿程序員使用全局變量澈吨,讓處理副作用變得很困難。并且函數(shù)的調用方式也很亂寄摆,有時候
g.area()
, 有時候area(g)
, 僅僅通過函數(shù)的調用方式上谅辣,完全看不出area
函數(shù)有沒有偷偷的改動g
本身的數(shù)據(jù)。 ? -
Shawn: Go里是通過接口來匹配婶恼,所以這里說的模板就是指接口桑阶。 ?
-
Shawn: 不大贊同這種比較。Go 里面的 interface 應該相當于 Elixir 里的 protocol勾邦。Interface 跟 protocol 的作用是相似的蚣录,用起來差不多,擴展性也差不多好眷篇。硬說 Go 里的 interface 是一種模式匹配的話萎河,也行,但要跟 Elixir 的模式匹配來比較就有點牽強蕉饼,他們不是一碼子事兒虐杯。 ?
-
Shawn: 代碼異味,意思是說代碼里的那些昧港,暗示了更深層次問題的表象擎椰。 ?
-
Shawn: 有個 Go 版的 supervisor tree,不知道多少人用過慨飘,效果怎樣确憨。 ?
-
Shawn: 搞不大明白這個說法,我理解 Go 里的 Panic 基本上跟其他語言里的 Exception 差不多瓤的。唯一不同的是 Try/Catch 是分支結構休弃,一個函數(shù)里可以寫很多個。但 panic()/defer()/recover() 是函數(shù)層級的圈膏,一個函數(shù)里只能有一個塔猾。 ?
-
Shawn: 當然還有其他問題的。比如遠程方法調用和本地調用的出錯模式不一樣稽坤,一個走網(wǎng)絡丈甸,一個走內存。你要是調用一個本地的方法尿褪,馬上就執(zhí)行完了睦擂;如果調用的是遠程的一個方法,可能幾毫秒還沒完杖玲。并且遠程方法可能由于各種原因(比如網(wǎng)絡超時)掛掉顿仇。這種情況下,你如果把 RPC 調用跟普通的 函數(shù)調用同等處理的話,那些遠程的調用可能會出一些隱藏的bug臼闻。在 Erlang 里鸿吆,所有進程間調用,不管是不是調用了遠程還是本地的函數(shù)述呐,處理起來一模一樣惩淳。 ?
-
Shawn: 假設有一個程序的兩個不同的版本:老版本 Blue,新版本 Green乓搬。在做升級部署的時候思犁,可以先把通往 Blue 的所有網(wǎng)絡信令都路由給 Green,如果出了問題馬上再切換回來进肯。 ?