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