為什么 Go 不是一款好的編程語(yǔ)言
我喜歡 Go. 常用它實(shí)現(xiàn)各種功能(包括在寫本文時(shí)的這個(gè)博客). Go 很實(shí)用,但不夠好。 不是說(shuō)它有多差, 只是沒(méi)那么好而已。
一門編程語(yǔ)言, 也許會(huì)用上一輩子, 所以選擇的時(shí)候要注意。
本文專注于 Go 的各種吐槽。 老生常談的有之,鮮為人知的也有。
我用 Rust 和Haskell 作為參照 (至少, 我以為, 這倆都很不錯(cuò))。 本文列出的所有問(wèn)題, 都有解決方案。
常規(guī)編程
那么問(wèn)題來(lái)了
我們寫代碼可以用于許多不同的事情。假如我寫了一個(gè)函數(shù)用來(lái)對(duì)一列數(shù)字求和,如果我可以用該函數(shù)對(duì)浮點(diǎn)數(shù)、整數(shù)以及其他任何類型進(jìn)行求和那該多棒。如果這些代碼包含了類型安全并且可以快速的寫出用于整型相加、浮點(diǎn)型相加等的獨(dú)立函數(shù)就更完美了。
好的解決方案:基于限制的泛型和基于參數(shù)的多態(tài)
到目前為止,我遇到的最好的泛型編程系統(tǒng)是rust和haskell所共用的那個(gè)。它一般被稱作”被限制的類型“。在haskell中,這個(gè)系統(tǒng)被稱作”type class“。而在Rust中,它被稱作”traits“。像這樣:
(Rust, version 0.11)
- fn id<T>(item: T) -> T {
- item
- }
(Haskell)
- id :: t -> t
- id a = a
在上面這個(gè)簡(jiǎn)單了例子中,我們定義了一個(gè)泛型函數(shù)id。id函數(shù)將它的參數(shù)原封不動(dòng)傳回來(lái)。很重要的一點(diǎn)是這個(gè)函數(shù)可以接受任何類型的參數(shù),而不是某個(gè)特定的類型。在Rust和haskell中,id函數(shù)保留了它參數(shù)的類型信息,使得靜態(tài)類型檢查可以順利工作,并且沒(méi)有為次在運(yùn)行期付出任何代價(jià)。你可以使用這個(gè)函數(shù)來(lái)寫一個(gè)克隆函數(shù)。
同樣,我們可以應(yīng)用這種方式來(lái)定義泛型數(shù)據(jù)結(jié)構(gòu)。例如:
(Rust)
- struct Stack<T>{
- items: Vec<T>
- }
(Haskell)
- data Stack t = Stack [t]
跟上面一樣,我們?cè)跊](méi)有運(yùn)行期額外消耗的情況下得到完全的靜態(tài)類型安全。
現(xiàn)在,如果我們想寫一個(gè)通用的函數(shù),我們必須告訴編譯器“這個(gè)函數(shù)只有在它的所有參數(shù)支持這個(gè)函數(shù)中所用用到的操作時(shí),才有意義”。舉個(gè)例子,如果我們想定義一個(gè)將它的三個(gè)參數(shù)相加,并返回其和的函數(shù),我們必須告訴編譯器:這三個(gè)參數(shù)必須支持加法運(yùn)算。就象這樣:
(Rust)
- fn add3<T:Num>(a:T, b:T, c:T)->T{
- a + b + c
- }
(Haskell)
- add3 :: Num t => t -> t -> t -> t
- add3 a b c = a + b + c
在上面這個(gè)例子中,我們告訴haskell的編譯器:“add3這個(gè)函數(shù)的參數(shù)必須是一個(gè)Num(算數(shù)數(shù)類型)“。因?yàn)榫幾g器知道一個(gè)Num類型的參數(shù)支持加法,所以這個(gè)函數(shù)的表達(dá)式可以通過(guò)類型檢查。在haskell中,這些限制也可應(yīng)用于data關(guān)鍵字所做的定義中。這是一個(gè)可以優(yōu)雅地定義百分之百類型安全的靈活泛型函數(shù)的方式。
go的解決方案:interface{}
Go的普通類型系統(tǒng)的結(jié)果是,Go對(duì)通用編程的支持很差。
你可以非常輕松的寫通用方程。假如你想寫一個(gè)可以打印被哈希的對(duì)象的哈希值。你可以定義一個(gè)擁有靜態(tài)類型安全保證的interface,像這樣:
(Go)
- type Hashable interface {
- Hash() []byte
- }
- func printHash(item Hashable) {
- fmt.Println(item.Hash())
- }
現(xiàn)在,你可以提供給printHash任何Hashable的對(duì)象,你也得到靜態(tài)類型檢查。這很好。
但如果你想寫一個(gè)通用的數(shù)據(jù)結(jié)構(gòu)呢?讓我們寫一個(gè)簡(jiǎn)單的鏈表。在Go里寫通用數(shù)據(jù)結(jié)構(gòu)的慣用方法是:
(Go)
- type LinkedList struct {
- value interface{}
- next *LinkedList
- }
- func (oldNode *LinkedList) prepend(value interface{}) *LinkedList {
- return &LinkedList{value, oldNode}
- }
- func tail(value interface{}) *LinkedList {
- return &LinkedList{value, nil}
- }
- func traverse(ll *LinkedList) {
- if ll == nil {
- return
- }
- fmt.Println(ll.value)
- traverse(ll.next)
- }
- func main() {
- node := tail(5).prepend(6).prepend(7)
- traverse(node)
- }
發(fā)現(xiàn)什么了嗎?value的類型是interface{}。interface{}就是所謂的“最高類型”,意味著所有其他的類型都是interface{}的子類型。這大致相當(dāng)于Java中的Object。呀?。ㄗ⒁猓簩?duì)于Go中是否有最高類型還有爭(zhēng)議,因?yàn)镚o宣稱沒(méi)有子類型。不管這些,保留類比的情況。
在Go里面“正確”構(gòu)建通用數(shù)據(jù)結(jié)構(gòu)的方法是將對(duì)象設(shè)置為最高類,然后把它們放入到數(shù)據(jù)結(jié)構(gòu)中。大約在2004年,Java就是這么做的。后來(lái)人們發(fā)現(xiàn)這完全違背了類型系統(tǒng)的本意。當(dāng)你有這樣的數(shù)據(jù)結(jié)構(gòu)時(shí),你完全消除了一個(gè)類型系統(tǒng)能提供的所有好處。比如,下面這個(gè)是完全有效的代碼:
- node := tail(5).prepend("Hello").prepend([]byte{1,2,3,4})
而這在一個(gè)良好結(jié)構(gòu)化的程序里完全沒(méi)有意義。你可能期望的時(shí)一個(gè)整數(shù)鏈表,但在某個(gè)情況下,一些疲憊、靠咖啡清醒的程序員在截止日期前偶然在某處加入了一個(gè)字符串。因?yàn)镚o里面的 通用數(shù)據(jù)結(jié)構(gòu)不知道它們值的類型,Go的編譯器也不會(huì)改正,你的程序在你失去從interface{}里面捕獲時(shí)將崩潰。
相同的問(wèn)題在任何通用數(shù)據(jù)結(jié)構(gòu)里都存在,無(wú)論是list、map、graph、tree、queue等。
語(yǔ)言可擴(kuò)展性
問(wèn)題
高級(jí)語(yǔ)言通常有復(fù)雜任務(wù)的關(guān)鍵字和符號(hào)簡(jiǎn)寫。比如,在很多語(yǔ)言中,迭代一個(gè)如數(shù)組一樣的數(shù)據(jù)集合中所有元素的簡(jiǎn)寫:
(Java)
- for (String name : names) { ... }
(Python)
- for name in names: ...
如果我們可以定義類型的相加也會(huì)很美好,那么我們可以這么做
(Python)
point3 = point1 + point2
好的解決方案:把運(yùn)算符視作函數(shù)
將內(nèi)建的運(yùn)算符和某個(gè)特別命名的函數(shù)對(duì)應(yīng)起來(lái),亦或?qū)㈥P(guān)鍵字視作特定函數(shù)的別名,這樣做可以很好的解決該問(wèn)題。
某些編程語(yǔ)言,像Python,Rust和Haskell允許我們重載運(yùn)算符。我們只需要給我們自定義的類添加一個(gè)函數(shù),自此,當(dāng)我們使用某個(gè)運(yùn)算符的時(shí)候(例如”+“),解釋器(編譯器)就會(huì)直接調(diào)用我們所添加的函數(shù)。在Python中,運(yùn)算符”+“對(duì)應(yīng)于__add__()函數(shù)。在Rust中,”+“運(yùn)算符在Add這個(gè)trait中定義為add()函數(shù)。在Haskell中,”+“對(duì)應(yīng)于Num這個(gè)type class中的(+)。
許多語(yǔ)言都有擴(kuò)展關(guān)鍵字的方法,例如for-each循環(huán)。Haskell沒(méi)有循環(huán),但是像Rust,Java和Python這樣的語(yǔ)言中都有”迭代器“這樣的概念使得for-each循環(huán)可以應(yīng)用于任何種類的數(shù)據(jù)集合結(jié)構(gòu)。
某些人可能會(huì)用這個(gè)特性做一些很操蛋的事情,這是一個(gè)潛在的缺點(diǎn)。例如,某些瘋狂的家伙使用”-“來(lái)代表兩個(gè)向量之間的點(diǎn)乘。但這并不完全是運(yùn)算符重載的問(wèn)題。無(wú)論使用何種語(yǔ)言,都可以寫出胡亂命名的函數(shù)。
Go的解決方案:沒(méi)有
Go語(yǔ)言不支持操作符重載或者關(guān)鍵字?jǐn)U展。
那么如果我們想給其他的東西(例如樹,鏈表)實(shí)現(xiàn)range關(guān)鍵字的操作怎么辦?太糟糕了。這不是語(yǔ)言的一部分。你這能在內(nèi)建對(duì)象上使用range關(guān)鍵字。對(duì)于關(guān)鍵字make也一樣,它不能給非內(nèi)建數(shù)據(jù)結(jié)構(gòu)申請(qǐng)內(nèi)存和初始化。
最接近這個(gè)可以使用迭代器的關(guān)鍵字的方式是寫一個(gè)包裝函數(shù),這個(gè)函數(shù)以目標(biāo)數(shù)據(jù)結(jié)構(gòu)為參數(shù)并返回一個(gè)可迭代的對(duì)象,我們通過(guò)使用這個(gè)對(duì)象在目標(biāo)數(shù)據(jù)結(jié)構(gòu)上迭代(譯者注:參見(jiàn)設(shè)計(jì)模式中的迭代器模式或C++中的迭代器實(shí)現(xiàn))。但是這樣做可能會(huì)很慢并且復(fù)雜,而且無(wú)法保證不引入其他的bug。
對(duì)于這樣一個(gè)問(wèn)題,有人辯解道,“這樣更容易讓人理解代碼,并且我看到的代碼就是真正被執(zhí)行的代碼。”也就是說(shuō),如果Go語(yǔ)言允許我們擴(kuò)展像range這樣的東西,那么range本身的機(jī)制和實(shí)現(xiàn)就會(huì)變得復(fù)雜難以理解。我認(rèn)為這樣的說(shuō)法沒(méi)有什么營(yíng)養(yǎng),因?yàn)椴还蹽o是否通過(guò)這種方式讓其變得更簡(jiǎn)單,更易懂,人們總要進(jìn)行這種在某些數(shù)據(jù)結(jié)構(gòu)上進(jìn)行迭代操作。如果我們不想把實(shí)現(xiàn)細(xì)節(jié)隱藏在range()函數(shù)里,我們就要把它隱藏在其他的工具函數(shù)里,沒(méi)什么改進(jìn)。所有的好代碼都是易讀的,大多數(shù)糟糕代碼讓人很難懂,很顯然Go不能改變這個(gè)事實(shí)。
基礎(chǔ)案例與失敗條件
那么問(wèn)題來(lái)了
當(dāng)遇到遞歸的數(shù)據(jù)結(jié)構(gòu)(如鏈表和樹)時(shí),我們希望找到一個(gè)途徑來(lái)指出我們到達(dá)數(shù)據(jù)結(jié)構(gòu)的末端。
當(dāng)遇到可能會(huì)執(zhí)行失敗的函數(shù)或包含缺失數(shù)據(jù)片的數(shù)據(jù)結(jié)構(gòu)時(shí),我們希望找到一個(gè)途徑明示我們遇到的幾種失敗情況。
Go 的方解決案: Nil (和多個(gè)返回值)
這回我先說(shuō) Go 的, 才好引出其他更好解決方案的討論.
Go 支持 null 指針(nil). 每次看到新的編程語(yǔ)言(如:tabula rasa), 實(shí)現(xiàn)這個(gè)導(dǎo)致 bug 滿天飛的功能, 我替他們可惜.
null 指針的歷史, 滿滿的都是 bug. 無(wú)論是歷史, 還是現(xiàn)實(shí), 我都看不出來(lái), 數(shù)據(jù)存在內(nèi)存地址為 0x0 的地方有什么意義. 指向 0x0 的指針通常都有特定的含義. 比如, 返回類型是指針的函數(shù)出錯(cuò), 會(huì)返回 0x0 . 遞歸數(shù)據(jù)結(jié)構(gòu)把 0x0 當(dāng)作基底(base case), 如: 樹結(jié)構(gòu)的頁(yè)節(jié)點(diǎn), 或鏈表的結(jié)尾. 這也是 null 指針在 Go 中的用法.
然而,這樣使用null指針也是不安全的。事實(shí)上,null指針是類型系統(tǒng)的后門,它讓你能夠創(chuàng)造某個(gè)根本不是所屬類型的實(shí)例。程序員有時(shí)候會(huì)忘記某個(gè)指針的值可能是null這個(gè)事實(shí),這是一個(gè)很常見(jiàn)的情況。在最好的情況下,你的程序會(huì)掛掉,而在最壞的情況下,這會(huì)產(chǎn)生一個(gè)可以被人利用的漏洞。編譯器無(wú)法輕易地阻止這種情況的發(fā)生,因?yàn)閚ull指針破壞了語(yǔ)言的類型系統(tǒng)。
對(duì)于Go來(lái)說(shuō),使用多重返回值這個(gè)機(jī)制,利用它第二個(gè)返回值來(lái)返回一個(gè)代表“失敗”的值是一個(gè)正確也被鼓勵(lì)的做法。然而,這種機(jī)制很容易被忽略或者誤用,并且在表示遞歸數(shù)據(jù)結(jié)構(gòu)的時(shí)候沒(méi)有什么用用處。
好的解決方案:代數(shù)數(shù)據(jù)類型和類型安全的錯(cuò)誤模式
我們可以使用類型系統(tǒng)來(lái)包裝錯(cuò)誤狀況,基底,而不是試圖打破類型系統(tǒng)。
現(xiàn)在我們想要構(gòu)建一個(gè)表示鏈表的類型。我們想表示兩種情況:我們是否已經(jīng)到達(dá)了鏈表的末尾,某個(gè)鏈表的節(jié)點(diǎn)上到底有沒(méi)有被存放在那里的數(shù)據(jù)。一種類型安全的方式是分別使用不同的類型來(lái)表示這些情況,最后將它們組合成一個(gè)單獨(dú)的類型(使用代數(shù)數(shù)據(jù)類型)?,F(xiàn)在我們有一個(gè)叫做Cons的類型來(lái)表示一個(gè)存放有某些數(shù)據(jù)的鏈表,一個(gè)叫做End的類型來(lái)表示鏈表的末尾。我們可以這樣寫:
(Rust)
- enum List<T> {
- Cons(T, Box<List<T>>),
- End
- }
- let my_list = Cons(1, box Cons(2, box Cons(3, box End)));
(Haskell)
- data List t = End | Cons t (List t)
- let my_list = Cons 1 (Cons 2 (Cons 3 End))
每個(gè)類型都為遞歸操作這個(gè)數(shù)據(jù)結(jié)構(gòu)的算法聲明了一個(gè)基底(End)。。Rust和Haskell都不允許null指針的出現(xiàn),所以我們永遠(yuǎn)都不會(huì)碰到null指針解引用所造成的bug(除非我們做一些很大膽的底層操作)。
這些代數(shù)數(shù)據(jù)結(jié)構(gòu)通過(guò)像模式匹配(后面講它)這樣的技術(shù),允許我們寫出非常明了的代碼。
那么,我們?nèi)绾蔚玫揭粋€(gè)可能返回或者不返回給定類型的數(shù)據(jù)的函數(shù),或是一個(gè)可能內(nèi)部包含或者沒(méi)有包含一個(gè)給定類型的數(shù)據(jù)的數(shù)據(jù)結(jié)構(gòu)呢?也就是說(shuō),我們?nèi)绾螌㈠e(cuò)誤狀況(failure condition)封裝到我們的類型系統(tǒng)中來(lái)呢?Rust使用Option,Haskell使用一個(gè)叫Maybe的類型來(lái)解決這個(gè)問(wèn)題。
我們想象這樣一個(gè)函數(shù),它所作的事情是搜索一個(gè)非空字符串的數(shù)組,尋找一個(gè)以這‘H’開(kāi)頭的字符串,返回第一個(gè)找到的這樣的字符串,如果沒(méi)有找到,就返回某種錯(cuò)誤狀況。在Go語(yǔ)言中,我們可以通過(guò)返回nil來(lái)表示“沒(méi)找到”這個(gè)錯(cuò)誤。但是在Haskell和Rust中,不使用危險(xiǎn)的指針,我們就可以安全地完成這個(gè)任務(wù)。
(Rust)
- fn search<'a>(strings: &'a[String]) -> Option<&'a str>{
- for string in strings.iter() {
- if string.as_slice()[0] == 'H' as u8 {
- return Some(string.as_slice());
- }
- }
- None
- }
(Haskell)
- search [] = Nothing
- search (x:xs) = if (head x) == 'H' then Just x else search xs
我們可以返回一個(gè)包含或者沒(méi)有包含一個(gè)字符串的對(duì)象來(lái)代替返回一個(gè)字符串或者null指針的做法。使用search()函數(shù)的程序員也會(huì)很清楚地知道這個(gè)函數(shù)可能會(huì)失敗(因?yàn)樗祷氐膶?duì)象的類型已經(jīng)這么說(shuō)了),而且程序員必須處理這兩種狀況,否則報(bào)錯(cuò)。這樣我們就跟null指針解引用所造成的bug說(shuō)再見(jiàn)了。
類型推導(dǎo)(Type Inference)
問(wèn)題
給程序中的每個(gè)值都指定類型, 有時(shí)看起來(lái)點(diǎn)過(guò)老土。 某些場(chǎng)合, 值的類型顯而易見(jiàn),如
int x = 5
y = x*2
這里的 y 明顯就是整形。更復(fù)雜點(diǎn)的,我們甚至可以根據(jù)函數(shù)的參數(shù)類型推斷出它的返回類型(反之亦然)。
出色的解決方案: 通用類型推導(dǎo)(General Type Inference)
Rust 和 Haskell 都基于 Hindley-Milner 類型系統(tǒng), 他們都很擅長(zhǎng)類型推導(dǎo), 你可以實(shí)現(xiàn)像下面這樣好玩的功能:
(Haskell)
- map :: (a -> b) -> [a] -> [b]
- let doubleNums nums = map (*2) nums
- doubleNums :: Num t => [t] -> [t]
函數(shù) (*2) 有一個(gè) Num 類型參數(shù), 返回也是一個(gè)Num 類型, Haskell 由此推斷 a 和 b 也是 Num 類型. 最后推斷出, 該函數(shù)有若干個(gè) Num 類型參數(shù), 返回若個(gè) Num 類型的值. 這種方式比 Go 和 C++ 的簡(jiǎn)單類型推導(dǎo)強(qiáng)大多了. 有了它, 哪怕是結(jié)構(gòu)復(fù)雜的程序, 就算我們不聲明這么多顯性類型, 編譯器也能正確處理.
Go 的解決方案 : :=
Go 支持 := 賦值操作符, 用法如下:
(Go)
- foo := bar()
它的原理是: 查找 bar() 的返回類型, 然后賦給 foo. 下列代碼的道理也一樣:
(C++)
- auto foo = bar();
沒(méi)什么稀奇的, 無(wú)非省去了人工查找函數(shù) bar() 的返回類型, 在鍵盤上多敲幾個(gè)字聲明 foo 的類型那點(diǎn)時(shí)間而已.
不變性(Immutability)
問(wèn)題
不變性是指,在程序生成的時(shí)候,設(shè)好的值,以后不會(huì)再變。 它的優(yōu)勢(shì)很明顯, 能減少因程序某個(gè)地方的數(shù)據(jù)結(jié)構(gòu)改變,導(dǎo)致另一個(gè)地方出現(xiàn)問(wèn)題的概率。
此外對(duì)程序優(yōu)化也有利。
出色的解決方案: 默認(rèn)使用不變性
程序員應(yīng)當(dāng)盡可能使用不可變數(shù)據(jù)結(jié)構(gòu)。 不變性使得判斷負(fù)面影響和安全性變得更簡(jiǎn)單。同時(shí)也能減少各種 Bug 。
Haskell 默認(rèn)情況下, 所有的值都是不可變的。改變數(shù)據(jù)結(jié)構(gòu)就意味著, 在保證正確性的前提下, 重新創(chuàng)建一個(gè)新的數(shù)據(jù)結(jié)構(gòu)。由于 Haskell 采用的是惰性求值(lazy evaluation)和永久性數(shù)據(jù)結(jié)構(gòu)(persistent data structures), 所以運(yùn)行的速度還是粉快的。Rust 屬于系統(tǒng)級(jí)編程語(yǔ)言。不可能使用惰性求值,也就不能像 Haskell 那樣始終使用不變性。 因此,雖然 Rust 默認(rèn)情況下,變量的值是不可變的。 但是,在需要的時(shí)候, 還是可以將變量設(shè)置成可變的。這樣挺好,因?yàn)樗仁钩绦騿T問(wèn)自己, 底需不需要將這個(gè)變量設(shè)成可變的。 這是很好的變成習(xí)慣, 對(duì)編譯器優(yōu)化代碼也有好處。
Go 的方案: 無(wú)
Go 不支持這項(xiàng)功能。
控制流結(jié)構(gòu)(Control Flow Structures)
問(wèn)題
控制流結(jié)構(gòu)是高級(jí)編程語(yǔ)言有別于匯編的原因之一. 它允許我們?cè)诔橄髮用? 有條理地控制程序流程. 毫無(wú)疑問(wèn), 所有高級(jí)語(yǔ)言都支持控制流結(jié)構(gòu), 否則, 我還說(shuō)個(gè)毛啊. 可惜, 有那么幾種相當(dāng)不錯(cuò)的控制流結(jié)構(gòu) Go 不支持.
出色的解決方案:模式匹配和復(fù)合表達(dá)式
模式匹配配合數(shù)據(jù)結(jié)構(gòu)或值使用的時(shí)候, 效果相當(dāng)好. 簡(jiǎn)直就是 case/switch 的加強(qiáng)版. 我們可以像這樣對(duì)值進(jìn)行匹配:
(Rust)
- match x {
- 0 | 1 => action_1(),
- 2 .. 9 => action_2(),
- _ => action_3()
- };
或者像這樣解構(gòu)數(shù)據(jù)結(jié)構(gòu)(deconstruct data structures):
(Rust)
- deg_kelvin = match temperature {
- Celsius(t) => t + 273.15,
- Fahrenheit(t) => (t - 32)/1.8 + 273.15
- };
上面的例子, 有時(shí)也稱作復(fù)合表達(dá)式. C 和 Go 中的 if 和 case/switch 語(yǔ)句只用來(lái)控制程序流程, 不會(huì)返回值; 而 Rust 和 Haskell 的 if 和 模式匹配語(yǔ)句則可以. 既然有值返回, 當(dāng)然也能用來(lái)賦給其他東東. 這里給出一個(gè) if 語(yǔ)句的例子:
(Haskell)
- x = if (y == "foo") then 1 else 2
Go 的方案: C語(yǔ)言風(fēng)格的無(wú)值語(yǔ)句( Valueless Statements)
不是我故意找 Go 的茬; 它確實(shí)有幾個(gè)不錯(cuò)的的控制流元素, 如, 用于并行計(jì)算的 select. 可惜沒(méi)有我鐘愛(ài)的復(fù)合表達(dá)式和模式匹配. Go 唯一支持賦值的語(yǔ)句, 是像這樣的原子表達(dá)式 x := 5 或 x := foo().
嵌入式編程
給嵌入式系統(tǒng)編寫程序與在一個(gè)有完整操作系統(tǒng)的計(jì)算機(jī)上編寫程序有很大不同。某些語(yǔ)言相比而言更適合嵌入式編程的需要。
對(duì)于不少人贊成Go語(yǔ)言可以給機(jī)器人編程這件事我很疑惑?;谝恍┰?,Go語(yǔ)言并不適合用來(lái)為嵌入式系統(tǒng)編寫程序。這一節(jié)并不是對(duì)Go語(yǔ)言的指責(zé),Go語(yǔ)言并不是被設(shè)計(jì)用來(lái)編寫嵌入式程序的語(yǔ)言。這一章節(jié)針對(duì)那些吹捧Go語(yǔ)言可以勝任嵌入式編程的人。
子問(wèn)題 #1:堆和動(dòng)態(tài)內(nèi)存分配
堆是一塊在運(yùn)行期創(chuàng)建的可以存儲(chǔ)任意數(shù)量對(duì)象的內(nèi)存區(qū)域。我們將對(duì)堆的使用稱作”動(dòng)態(tài)內(nèi)存分配“。
通常,在嵌入式系統(tǒng)中使用堆存儲(chǔ)空間是不明智的。較大的內(nèi)存開(kāi)銷和需要管理復(fù)雜的數(shù)據(jù)結(jié)構(gòu)是主要的原因,尤其是當(dāng)你在一塊主頻只有8MHz,RAM只有2KB的MCU上寫程序的時(shí)候。
在實(shí)時(shí)系統(tǒng)(因?yàn)槟骋徊僮骱臅r(shí)過(guò)長(zhǎng)就可能會(huì)跪的系統(tǒng))中使用堆也是不明智的,因?yàn)閷?duì)堆上空間的申請(qǐng)和釋放所消耗的時(shí)間有很大的不確定性。舉個(gè)例子,如果你的MCU正在控制一個(gè)火箭的引擎,就在這時(shí),如果一個(gè)對(duì)??臻g的申請(qǐng)比平常多消耗了幾百毫秒,導(dǎo)致對(duì)閥門的錯(cuò)誤計(jì)時(shí),就會(huì)發(fā)生大爆炸。
還有一些原因致使動(dòng)態(tài)內(nèi)存分配對(duì)嵌入式編程沒(méi)有多大用。例如,許多使用堆的語(yǔ)言同時(shí)也擁有垃圾收集機(jī)制。垃圾收集機(jī)制經(jīng)常會(huì)暫停整個(gè)程序一會(huì)兒,在堆上尋找垃圾(不再被程序使用的內(nèi)存)并清除它們。這比單純的堆空間申請(qǐng)更加具有不確定性。
好的解決方案:讓動(dòng)態(tài)內(nèi)存分配成為可選項(xiàng)
Rust語(yǔ)言的標(biāo)準(zhǔn)庫(kù)中有很多特性依賴于堆。然而,Rust語(yǔ)言的編譯器支持完全關(guān)閉這些有關(guān)堆的語(yǔ)言特性,并且能夠靜態(tài)地確保這些特性在程序中不被使用。寫出完全不使用堆的Rust程序是完全可行的。
Go語(yǔ)言的解決方案:沒(méi)有
Go語(yǔ)言嚴(yán)重依賴于對(duì)堆的運(yùn)用。沒(méi)有可行的方式讓Go程序完全不使用堆。這不是Go語(yǔ)言的問(wèn)題。這在Go語(yǔ)言的目的應(yīng)用領(lǐng)域完全沒(méi)有問(wèn)題。
Go并不是一門實(shí)時(shí)的語(yǔ)言,通常我們不能擔(dān)保合理復(fù)雜的Go程序的執(zhí)行時(shí)間。這可能有點(diǎn)費(fèi)解,我來(lái)解釋一下:Go相對(duì)而言很快,但不是實(shí)時(shí)的,這兩個(gè)概念非常不同。執(zhí)行速度快對(duì)嵌入式程序來(lái)說(shuō)很重要,但是真正重要的是能否擔(dān)保某些操作的最大執(zhí)行時(shí)間,而這恰恰是Go不能預(yù)測(cè)的。這個(gè)問(wèn)題有很大一部分是Go語(yǔ)言對(duì)于堆空間和垃圾收集機(jī)制的使用造成的。
Haskell也有相似的問(wèn)題。Haskell同樣由于對(duì)堆的大量使用而不能勝任嵌入式或者實(shí)時(shí)編程。然而,我沒(méi)有看見(jiàn)任何人推薦使用Haskell對(duì)機(jī)器人編程,所以我不用指出這點(diǎn)。
子問(wèn)題#2:不安全的底層代碼
當(dāng)我們寫嵌入式程序的時(shí)候,寫一些不安全的代碼(不安全的的類型轉(zhuǎn)換,或者指針運(yùn)算)是不可避免的。在C或C++中,做這樣的事情是很簡(jiǎn)單的。如果我需要向0x1234這個(gè)內(nèi)存地址寫入0xff這個(gè)值來(lái)點(diǎn)亮一個(gè)LED,我可以這樣寫:
(C/C++)
*(uint8_t*)0x1234 = 0xFF;
這樣做很危險(xiǎn),只有當(dāng)我們寫非常底層的系統(tǒng)代碼的時(shí)候才有意義。這就是Go和Haskell沒(méi)有簡(jiǎn)單的方式來(lái)做這樣的事的原因:它們不是系統(tǒng)編程語(yǔ)言。
好的解決方案:將不安全的代碼孤立開(kāi)來(lái)
注重安全和系統(tǒng)編程的Rust語(yǔ)言有一個(gè)非常好的解決方案:unsafe代碼塊。unsafe代碼塊是一種顯示地將不安全的代碼分離出來(lái)的方式。我們通過(guò)如下的方式在Rust語(yǔ)言中向0x1234地址寫入0xff:
(Rust)
- unsafe{
- *(0x1234 as *mut u8) = 0xFF;
- }
如果我們?cè)趗nsafe代碼塊外面做這樣的事情,Rust的編譯器會(huì)警告我們。這樣允許我們?cè)跐M足嵌入式編程需要的同時(shí),保持了程序的安全和穩(wěn)定。
Go的解決方案:沒(méi)有
Go語(yǔ)言本來(lái)就不是為了做這樣的事而出現(xiàn)的,所以沒(méi)有任何內(nèi)建的支持。
總結(jié)
現(xiàn)在你可能會(huì)說(shuō),“那么為什么你說(shuō)Go語(yǔ)言不好?這只是一大堆你的抱怨而已。你可以針對(duì)任何語(yǔ)言發(fā)牢騷。“沒(méi)有語(yǔ)言是完美的,這很正確。然而,我希望我的抱怨能在某種程度上說(shuō)明:
Go語(yǔ)言本質(zhì)上沒(méi)有干了什么新的事情
Go語(yǔ)言本身沒(méi)有被良好地設(shè)計(jì)
Go語(yǔ)言是其他現(xiàn)代編程語(yǔ)言的退化
原文鏈接:http://www.oschina.net/translate/why-go-is-not-good
英文原文:Why Go Is Not Good


























