為什麼Go是一種設計糟糕的程式語言

王子健發表於2016-06-27

好吧,我承認這個標題有點放肆。我多告訴你一點:我愛肆意妄言的標題,它能夠吸引注意力。不管怎樣,在這篇博文中我會試圖證明 Go 是一個設計得很糟糕的語言(劇透:事實上它是)。我已經擺弄 Go 有幾個月了,而且,我想我在六月某個時候執行了第一個 helloworld 程式。雖然我的數學不太好,但在那之後已經有四個月了,並且我的 Github 上已經有了幾個 package。不必多說,我仍完全沒有在生產中使用 Go 的經驗,所以把我說的有關 “編碼支援”、“部署”以及相關內容當作不可盡信的吧。

我喜歡 Go語言。自從試用了它以後我就愛上了。我花了幾天來接受 Go 的語言習慣,來克服沒有泛型的困難,瞭解奇怪的錯誤處理和 Go 的所有典型問題。我讀了 Effective Go,以及 Dave Cheney 的部落格上的許多文章,而且注意與 Go 有關的一切動向等等。我可以說我是一個活躍的社群成員!我愛 Go 而且我無法自拔—Go 令人驚奇。然而依我拙見,與它所宣傳的正好相反,Go 是一個設計糟糕、劣質的語言。

Go 被認為是一個簡練的程式語言。根據 Rob Pike 所說,他們使出了渾身解數來使這個語言的規範簡單明瞭。這門語言的這一方面是令人驚奇的:你可以在幾小時內學會基礎並且直接開始編寫能執行的程式碼,大多數情況下 Go 會如所期待的那樣工作。你會被激怒,但是希望它管用。現實並不一樣,Go語言並不是一個簡潔,它只是低劣。以下有一些論點來證明。

理由1. 切片(Slice)操作壓根就不對!

切片很棒,我真的很喜歡這個概念和一些用法。但是讓我們花一秒鐘,想象一下我們真的想要去用切片寫一些程式碼。顯而易見,切片存在於這門語言的靈魂中,它讓 Go 強大。但是,再一次,在“理論”討論的間隙,讓我們想象一下我們有時會寫一些實實在在的程式碼。以下列出的程式碼展示了你在 Go 中如何做列表操作。

信不信由你,這是 Go 程式設計師每天如何轉換切片的真實寫照。而且我們沒有任何泛型機制,所以,哥們,你不能創造一個漂亮的 insert() 函式來掩蓋這個痛苦。我在 playgroud 貼了這個,所以你不應該相信我:自己雙擊一下去親自看看。

理由2. Nil 介面並不總是 nil :)

他們告訴我們“在 Go 中錯誤不只是字串”,並且你不該把它們當字串對待。比如,來自 Docker 的 spf13 在他精彩的“Go 中的7個失誤以及如何避免”中如此講過。

他們也說我應該總是返回 error 介面型別(為了一致性、可讀性等等)。我在以下所列程式碼中就是這麼做的。你會感到驚訝,但是這個程式真的會跟 Pike 先生 say hello,但是這是所期待的嗎?

是的,我知道為什麼這會發生,因為我閱讀了一堆複雜的關於介面和介面在 Go 中如何工作的資料。但是對於一個新手……拜託哥們,這是當頭一棒!實際上,這是一個常見的陷阱。如你所見,沒有這些讓人心煩意亂的特性的 Go 是一個直接易學的語言,它偶爾說 nil 介面並不是nil ;)

理由3. 可笑的變數覆蓋

為了以防萬一你對這個術語不熟悉,讓我引用一下 Wikipedia:”當在某個作用域(判定塊、方法或者內部類)中宣告的一個變數與作用域外的一個變數有相同的名字,變數覆蓋就會發生。“看上去挺合理,一個相當普遍的做法是,多數的語言支援變數覆蓋而且這沒有問題。Go 並不是例外,但是卻不太一樣。下面是覆蓋如何工作的:

是的,我也認識到 := 操作符製造了一個新的變數並且賦了一個右值,所以根據語言規範這是一個完全合法的行為。但是這裡有件有意思的事:試著去掉內部作用域——它會如期望的執行(”在42之後“)。否則,就跟變數覆蓋問個好吧。

無需贅言,這不是什麼我在午飯時想起來的一個好玩的例子,它是人們早晚會遇到的真實的東西。這周的早些時候我重構了一些 Go 程式碼,就遇到了整個問題兩次。編譯沒問題,程式碼檢查沒問題,什麼都沒問題——程式碼就是不正常執行。

理由4. 你不能傳遞把 []struct 作為 []interface 傳遞

介面很棒,Pike&Co. 一直說它就是 Go 語言的一切:介面事關你如何處理泛型,如何做 mock 測試,它是多型的實現方法。讓我告訴你吧,當我閱讀“Effective Go”的時候我真心愛著介面,而且我一直愛著它。除了上面我提出的“nil 介面不是 nil”的問題外,這裡有另一個令人討厭的事讓我認為介面在 Go 語言中沒有得到頭等支援。基本上,你不能傳遞一個結構的切片到一個接收介面型別切片的函式上:

不出意外,這是個已知的根本沒有被當作問題的問題。它只是 Go 的又一個可笑的事,對吧?我真的推薦你閱讀一下相關的 wiki,你會發現為什麼“傳遞結構切片作為藉口切片”不可行。但是呀,好好想想!我們可以做到,這裡沒什麼魔法,這只是編譯器的問題。看,在 49-57行 我做了一個由 []struct 到 []interface的顯式轉換。為什麼 Go 編譯器不為我做這些?是的顯示要比隱式好,但是WTF?

我只是無法忍受人們看著這種狗屁語言又一直說“好,挺好的”。並不是。這些讓 Go 變成了一個糟糕的語言。

理由5. 不起眼的 range“按值”迴圈

這是我曾經遇到過的第一個語言問題。好吧,在 Go 中有一個 “for-range”迴圈,是用來遍歷切片和監聽 channel 的。它到處都用得到而且還不錯。然而這裡有一個小問題,大多數新手被坑在這上面:range 迴圈只是按值的,它只是值拷貝,你不能真的去做什麼,它不是 C++ 中的 foreach。

請注意,我沒有抱怨 Go 裡沒有按引用的 range,我抱怨的是 range 太不起眼。動詞“range”有點像是說“遍歷專案“,而不是”遍歷專案的拷貝“。讓我們看一眼”Effective Go“中的 For,它聽起來一點也不像”遍歷切片中的拷貝值“,一點也不。我同意這是個小問題,我很快(幾分鐘)就克服了它,但是沒有經驗的 gopher 也許會花上一些時間除錯程式碼,驚訝於為什麼值沒有改變。你們至少可以在”Effective Go“裡面把這點講述明白。

理由6. 可疑的編譯器嚴謹性

就如我之前已經告訴你的,Go被認為是一個有著嚴謹的編譯器的,簡單明瞭並且可讀性高的語言。比如,你不能編譯一個帶有未使用的 import 的程式。為什麼?只是因為 Pike 先生認為這是對的。信不信由你,未使用的 import 不是世界末日,我完全可以與其共存。我完全同意它不對而且編譯器不惜列印出相關的警告,但是為什麼你為了這麼一個小事中止編譯?就為了未使用的 import,當真?

Go1.5 引入了一個有趣的語言變化:現在你可以列出 map 字面量,而不必顯示列出被包含的型別名。這花了他們五年(甚至更多)來認識到顯示型別列出被濫用了。

另一個我在 Go 語言裡非常享受的事情:逗號。你看,在 Go 中你可以自由地定義多行 import、const 或者 var 程式碼塊:

好吧,這挺好的。但是一旦它涉及到“可讀性”,Rob Pike 認為加上逗號會很棒。某一刻,在加上逗號以後,他決定你應該也把結尾的逗號留著!所以你並不這樣寫:

你必須這樣寫:

我仍然懷疑為什麼我們在 import/var/consts 程式碼塊中可以忽略逗號,但是在列表和對映中不能。無論如何,Rob Pike 比我清楚!可讀性萬歲!

理由7. Go generate 太詭異了

首先,你要知道我沒有反對程式碼生成。對於 Go 這樣一個粗劣的語言,這也許是僅有的可用來避免拷貝-貼上一些常見的東西的途徑。然而,Go:generate——一個 Go 使用者到處都用的程式碼生成工具,現在僅僅是垃圾而已。好吧,公平來說,這個工具本身還好,我喜歡它。而整個的方式是錯的。我們看看吧,你要通過使用特別的魔法命令來生成一些程式碼。對,通過程式碼註釋中的一些神奇的位元組序列來做程式碼生成。

註釋是用來解釋程式碼,而不是生成程式碼。不過神奇的註釋在當今的 Go 中是一種現象了。非常有意思的是,沒人在乎,大家覺得這就挺好的。依我愚見,這絕對比嚇人的未使用的 import 要糟糕。

後記

如你所見,我沒有抱怨泛型、錯誤處理、語法糖和其他 Go 相關的典型問題。我同意泛型不至關重要,但如果你去掉泛型,請給我們一些正常的程式碼生成工具而不是隨機的亂七八糟的狗屎神奇註釋。如果你去掉異常,請給我們安全地把介面與 nil 比較的能力。如果你去掉語法糖,請給我們一些能夠如預期工作的程式碼,而不是一些像變數遮蔽這樣的“哎呦臥槽“的東西。

總而言之,我會繼續使用 Go。理由如下:因為我愛它。我恨它因為它就是堆垃圾,但是我愛它的社群,我愛它的工具,我愛巧妙的設計決定(介面你好)和整個生態。

嘿夥計,想嘗試嘗試 Go 嗎?

(譯者注:原文一些理由並不站得住腳,原文連結中的評論也值得一看。)

相關文章