之前寫過一些對 Go 語言的負面評價。如今看來,雖然那些評價大部分屬實,然而卻因爲言辭激烈,沒有點明具體問題,難以讓某些人信服。在通過幾個月實際使用 Go 來構造網站以後,我以爲如今是時候對它做一些更加「客觀」的評價了。html
Go 比起 C 和 C++ 確實有它的優勢,這是很顯然的事情。它比起 Java 也有少數優勢,然而相對而言更可能是不足之處。因此我對 Go 的偏好在比 Java 稍低一點的位置。程序員
Go 語言比起 C,C++ 的強項,固然是它的簡單性和垃圾回收。因爲 C 和 C++ 的設計有不少歷史遺留問題,因此 Go 看起來確實更加優雅和簡單。比起那些大量使用設計模式的 Java 代碼,Go 語言的代碼也彷佛更簡單一些。另外,Go 的垃圾回收機制比起 C 和 C++ 的全手動內存管理來講,大大下降了程序員的頭腦負擔。golang
可是請注意,這裏的所謂「優勢」都是相對於 C 之類的語言而言的。若是比起另外的一些語言,Go 的這種優勢也許就很微不足道,甚至是歷史的倒退了。算法
Go 的簡單性體如今它的語法和語義的某些方面。Go 的語法比 C 要稍好一些,有少數比 Java 更加方便的設計,然而卻也有「倒退」的地方。並且這些倒退還不被不少人認爲是倒退,反而認爲是進步。我如今舉出暫時能想得起來的幾個方面:編程
進步:Go 有語法支持一種相似 struct literal 的構造,好比你能夠寫這樣的代碼來構造一個 S struct:設計模式
S { x: 1, y: 2, }
這比起 Java 只能用構造函數來建立對象是一個不錯的方便性上的改進。這些東西可能借鑑於 JavaScript 等語言的設計。數組
倒退:類型放在變量後面,卻沒有分隔符。若是變量和它的類型寫成像 Pascal 那樣的,好比 x : int
,那也許還好。然而 Go 的寫法倒是 x int
,沒有那個冒號,並且容許使用 x, y int
這樣的寫法。這種語法跟 var,函數參數組合在一塊兒以後,就產生了擾亂視線的效果。好比你能夠寫一個函數是這樣開頭的:數據結構
func foo(s string, x, y, z int, c bool) {
...
}
注意 x, y, z 那個位置,實際上是很混淆的。由於看見 x
的時候我不能當即從後面那個符號(, y
)看到它是什麼類型。因此在 Go 裏面我推薦的寫法是把 x
和 y
徹底分開,就像 C 和 Java 那樣,不過類型寫在後面:併發
func foo(s string, x int, y int, z int, c bool) {
...
}
這樣一來就比較清晰了,雖然我願意再多寫一些冒號。每個參數都是「名字 類型」的格式,因此我一眼就看到 x 是 int。雖然多打幾個字,然而節省的是「眼球 parse 代碼」的開銷。框架
倒退:類型語法。Go 使用像 []string
這樣的語法來表示類型。不少人說這種語法很是「一致」,但通過一段時間我卻沒有發現他們所謂的一致性在哪裏。其實這樣的語法很難讀,由於類型的各部分之間沒有明確的分隔標識符,若是和其餘一些符號,好比 * 搭配在一塊兒,你就須要知道一些優先級規則,而後費比較大的功夫去作「眼球 parse」。好比,在 Go 代碼裏你常常看到 []*Struct
這樣的類型,注意 *Struct
要先結合在一塊兒,再做爲 []
的「類型參數」。這種語法缺少足夠的分隔符做爲閱讀的「邊界信號」,一旦後面的類型變得複雜,就很難閱讀了。好比,你能夠有 *[]*Struct
或者 *[]*pkg.Struct
這樣的類型。因此這其實還不如像 C++ 的 vector<struct*>
這樣的寫法,也就更不如 Java 或者 Typed Racket 的類型寫法來得清晰和簡單。
倒退:過分地「語法重載」,好比 switch, for 等關鍵字。Go 的 switch 關鍵字其實包含了兩種不一樣的東西。它能夠是 C 裏面的普通的 switch(Scheme 的 case),也能夠是像 Scheme 的 cond 那樣的嵌套分支語句。這兩種語句實際上是語義徹底不一樣的,然而 Go 的設計者爲了顯得簡單,把它們合二爲一,而其實引發了更大的混淆。這是由於,就算你把它們合二爲一,它們仍然是兩種不一樣的語義結構。把它們合併的結果是,每次看到 switch 你都須要從它們「頭部」的不一樣點把這兩種不一樣的結構區分開來,增長了人腦的開銷。正確的做法是把它們分開,就像 Scheme 那樣。其實我設計語言的時候有時候也犯一樣的錯誤,覺得兩個東西「本質」上是同樣的,因此合二爲一,結果通過一段時間,發現實際上是不同的。因此不要小看了 Scheme,不少你認爲是「新想法」的東西,其實早就被它那很是嚴謹的委員會給拋棄在了歷史的長河中。
Go 語言裏面還有其餘一些語法設計問題,好比強制把 {
放在一行以後並且不能換行,if 語句的判斷開頭能夠嵌套賦值操做等等。這些試圖讓程序顯得短小的做法,其實反而下降了程序理解的流暢度。
因此總而言之,Go 的語法很難被叫作「簡單」或者「優雅」,它的簡單性其實在 Java 之下。
Go 提供了一些比較方便的工具。好比 gofmt,godef 等,使得 Go 代碼的編程比起單用 Emacs 或者 VIM 來編輯 C 和 C++ 來講是一個進步。使用 Emacs 編輯 Go 就已經能實現某些 IDE 纔有的功能,好比精確的定義跳轉等等。
這些工具雖然好用,但比起像 Eclipse, IntelliJ 和 Visual Studio 這樣的 IDE,差距仍是至關大的。比起 IDE,Go 的工具鏈缺少各類最基本的功能,好比列出引用了某個變量的全部位置,重命名等 refactor 功能,好用的 debugger (GDB 不算好用)等等。
Go 的各類工具感受都不大成熟,有時候你發現有好幾個不一樣的 package 用於解決同一個問題,搞不清楚哪個好些。並且這些東西配置起來不是那麼的可靠和簡單,都須要折騰。每個小功能你都得從各處去尋找 package 來配置。有些時候一個工具配置了以後其實沒有起做用,要等你摸索好半天才發現問題出如今哪裏。這種沒有組織,沒有計劃的工具設計,是很難超過專業 IDE 廠商的連貫性的。
Go 提供了方便的 package 機制,能夠直接 import 某個 GitHub repository 裏的 Go 代碼。不過我發現不少時候這種 package 機制帶來的更可能是麻煩事和依賴關係。因此 Go 的推崇者們又設計了一些像 godep 的工具,用來繞過這些問題,結果 godep 本身也引發一些稀奇古怪的問題,致使有時候新的代碼其實沒有被編譯,產生莫名其妙的錯誤信息(多是因爲 godep 的 bug)。
我發現不少人看到這些工具以後老是很狂熱的認爲它們就能讓 Go 語言一統天下,其實還差得很是之遠。並且如此年輕的語言就已經出現這麼多的問題,我以爲全部這些麻煩事累積下來,多年之後恐怕夠嗆。
比起 C 和 C++ 徹底手動的內存管理方式,Go 有垃圾回收(GC)機制。這種機制大大減輕了程序員的頭腦負擔和程序出錯的機會,因此 Go 對於 C/C++ 是一個進步。
然而進步也是相對的。Go 的垃圾回收器是一個很是原始的 mark-and-sweep,這比起像 Java,OCaml 和 Chez Scheme 之類的語言實現,其實還處於起步階段。
固然若是真的遇到 GC 性能問題,經過大量的 tuning,你能夠部分的改善內存回收的效率。我也看到有人寫過一些文章介紹他們如何作這些事情,然而這種文章的存在說明了 Go 的垃圾回收還很是不成熟。GC 這種事情我以爲大部分時候不該該是讓程序員來操心的,不然就失去了 GC 比起手動管理的不少優點。因此 Go 代碼想要在實時性比較高的場合,仍是有很長的路要走的。
因爲缺少先進的 GC,卻又帶有高級的抽象,因此 Go 其實無法取代 C 和 C++ 來構造底層系統。Go 語言的定位對我來講愈來愈模糊。
比起 C++ 和 Java 來講,Go 缺少 generics。雖然有人討厭 Java 的 generics,然而它自己卻不是個壞東西。Generics 其實就是 Haskell 等函數式語言裏面所謂的 parametric polymorphism,是一種很是有用的東西,不過被 Java 抄去以後有時候沒有作得全對。由於 generics 可讓你用同一塊代碼來處理多種不一樣的數據類型,它爲避免重複,方便替換複雜數據結構等提供了方便。
因爲 Go 沒有 generics,因此你不得不重複寫不少函數,每個只有類型不一樣。或者你能夠用空 interface {},然而這個東西其實就至關於 C 的 void* 指針。使用它以後,代碼的類型沒法被靜態的檢查,因此其實它並無 generics 來的嚴謹。
比起 Java,Go 的不少數據結構都是「hard code」進了語言裏面,甚至創造了特殊的關鍵字和語法來構造它們(好比哈希表)。一旦遇到用戶須要本身定義相似的數據結構,就須要把大量代碼重寫一遍。並且因爲沒有相似 Java collections 的東西,沒法方便的換掉複雜的數據結構。這對於構造像 PySonar 那樣須要大量實驗才能選擇正確的數據結構,須要實現特殊的哈希表等數據結構的程序來講,Go 語言的這些缺失會是一個很是大的障礙。
缺乏 generics 是一個問題,然而更嚴重的問題是 Go 的設計者及其社區對於這類語言特性的盲目排斥。當你提到這些,Go 支持者就會以一種蔑視的態度告訴你:「我看不到 generics 有什麼用!」這種態度比起語言自己的缺點來講更加有害。在通過了很長一段時間以後 Go 語言的設計者們開始考慮加入 generics,而後因爲 Go 的語法設計偷工減料,再加上因爲缺少 generics 而產生的特例(好比 Go 的 map 的語法設計)已經被大量使用,我以爲要加入 generics 的難度已經很是大。
Go 和 Unix 系統同樣,在出現的早期就已經由於不吸收前人的教訓,背上了沉重的歷史包袱。
不少人都以爲 Go 的多返回值設計是一個進步,然而這裏面卻有不少蹊蹺的東西。且不說這根本不是什麼新東西(Scheme 很早就有了多返回值 let-values),Go 的多返回值卻被大量的用在了錯誤的地方—Go 利用多返回值來表示出錯信息。好比 Go 代碼裏最多見的結構就是:
ret, err := foo(x, y, z)
if err != nil {
return err
}
若是 foo
的調用產生了錯誤,那麼 err
就不是 nil。Go 要求你在定義了變量以後必須使用它,不然報錯。這樣它「碰巧」避免了出現錯誤 err
而不檢查的狀況。不然若是你想忽略錯誤,就必須寫成
ret, _ := foo(x, y, z)
這樣當 foo 出錯的時候,程序就會自動在那個位置當掉。
不得不說,這種「歪打正着」的作法雖然貌似可行,從類型系統角度看,倒是很是不嚴謹的。由於它根本不是爲了這個目的而設計的,因此你能夠比較容易的想出各類辦法讓它失效。並且因爲編譯器只檢查 err
是否被「使用」,卻不檢查你是否檢查了「全部」可能出現的錯誤類型。好比,若是 foo 可能返回兩種錯誤 Error1 和 Error2,你無法保證調用者徹底排除了這兩種錯誤的可能性以後才使用數據。因此這種錯誤檢查機制其實還不如 Java 的 exception 來的嚴謹。
另外,ret
和 err
同時被定義,而每次只有其中一個不是 nil,這種「或」的關係並非靠編譯器來保障,而是靠程序員的「約定俗成」。這樣當 err
不是 nil 的時候,ret
其實也能夠不是 nil。這些組合帶來了挺多的混淆,讓你每次看到 return 的地方都不確信它到底想返回一個錯誤仍是一個有效值。若是你意識到這種「或」關係其實意味着你只應該用一個返回值來表示它們,你就知道其實 Go 誤用了多返回值來表示可能的錯誤。
其實若是一個語言有了像 Typed Racket 和 PySonar 所支持的 「union type」類型系統,這種多返回值就沒有意義了。由於若是有了 union type,你就能夠只用一個返回值來表示有效數據或者錯誤。好比你能夠寫一個類型叫作 {String, FileNotFound},用於表示一個值要麼是 String,要麼是 FileNotFound 錯誤。若是一個函數有可能返回錯誤,編譯器就強制程序員檢查全部可能出現的錯誤以後才能使用數據,從而能夠徹底避免以上的各類混淆狀況。對 union type 有興趣的人能夠看看 Typed Racket,它擁有我迄今爲止見過最強大的類型系統(超越了 Haskell)。
因此能夠說,Go 的這種多返回值,實際上是「歪打」打着了一半,而後換着法子繼續歪打,而不是瞄準靶心。
Go 採用了基於接口(interface)的面向對象設計,你可使用接口來表達一些想要進行抽象的概念。
然而這種接口設計卻不是沒有問題的。首先跟 Java 不一樣,實現一個 Go 的接口不須要顯式的聲明(implements),因此你有可能「碰巧」實現了某個接口。這種不肯定性對於理解程序來講是有副作用的。有時候你修改了一個函數以後就發現編譯不經過,抱怨某個位置傳遞的不是某個須要的接口,然而出錯信息卻不能告訴你準確的緣由。要通過一番摸索你才發現你的 struct 爲何再也不實現以前定義的一個接口。
另外,有些人使用接口,不少時候不過是爲了傳遞一些函數做爲參數。我有時候不明白,這種對於函數式語言再簡單不過的事情,在 Go 語言裏面爲何要另外定義一個接口來實現。這使得程序不如函數式語言那麼清晰明瞭,並且修改起來也很不方便。有不少冗餘的名字要定義,冗餘的工做要作。
舉一個相關的例子就是 Go 的 Sort 函數。每一次須要對某種類型 T
的數組排序,好比 []string
,你都須要
TSorter
,好比 StringSorter
StringSorter
類型定義三個方法,分別叫作 Len
, Swap
, Less
[]string
cast 成 StringSorter
sort.Sort
對這個數組排序想一想 sort 在函數式語言裏有多簡單吧?好比,Scheme 和 OCaml 均可以直接這樣寫:
(sort '(3 4 1 2) <)
這裏 Scheme 把函數 <
直接做爲參數傳給 sort 函數,而沒有包裝在什麼接口裏面。你發現了嗎,Go 的那個 interface 裏面的三個方法,其實原本應該做爲三個參數直接傳遞給 Sort,但因爲受到 design pattern 等思想的侷限,Go 的設計者把它們「打包」做爲接口來傳遞。並且因爲 Go 沒有 generics,你沒法像函數式語言同樣寫這三個函數,接受比較的「元素」做爲參數,而必須使用它們的「下標」。因爲這些方法只接受下標做爲參數,因此 Sort 只能對數組進行排序。另外因爲 Go 的設計比較「底層」,因此你須要另外兩個參數: len 和 swap。
其實這種基於接口的設計其實比起函數式語言,差距是很大的。比起 Java 的接口設計,也能夠說是一個倒退。
Goroutine 能夠說是 Go 的最重要的特點。不少人使用 Go 就是據說 goroutine 能支持所謂的「大併發」。
首先這種大併發並非什麼新鮮東西。每一個理解程序語言理論的人都知道 goroutine 其實就是一些用戶級的 「continuation」。系統級的 continuation 一般被叫作「進程」或者「線程」。Continuation 是函數式語言專家們再瞭解不過的東西了,好比個人前導師 Amr Sabry 就是關於 continuation 的頂級專家之一。
Node.js 那種 「callback hell」,其實就是函數式語言裏面經常使用的一種手法,叫作 continuation passing style (CPS)。因爲 Scheme 有 call/cc,因此從理論上講,它能夠不經過 CPS 樣式的代碼而實現大併發。因此函數式語言只要支持 continuation,就會很容易的實現大併發,也許還會更高效,更好用一些。好比 Scheme 的一個實現 Gambit-C 就能夠被用來實現大併發的東西。Chez Scheme 也許也能夠,不過還有待確認。
固然具體實現上的效率也許有區別,然而我只是說,goroutine 其實並非像不少人想象的那樣全新的,革命性的,獨一無二的東西。只要有足夠的動力,其它語言都能添加這個東西。
Go 實現了 defer 函數,用於避免在函數出錯後忘了收拾殘局(cleanup)。然而我發現這種 defer 函數有被濫用的趨勢。好比,有些人把那種不是 cleanup 的動做也作成 defer,到後來累積幾個 defer 以後,你就再也不能一眼看得清楚到底哪塊代碼先運行哪塊後運行了。位置處於前面的代碼竟然能夠在後來運行,違反了代碼的天然位置順序關係。
固然這能夠怪程序員不明白 defer 的真正用途,然而一旦你有了這種東西就會有人想濫用它。那種急於試圖利用一個語言的每種 feature 的人,特別喜歡幹這種事情。這種問題恐怕須要不少年的經驗以後,纔會有人寫成書來教育你們。在造成統一的「代碼規範」之前,我預測 defer 仍然會被大量的濫用。
因此咱們應該想一下,爲了不可能出現的資源泄漏,defer 帶來的究竟是利多仍是弊多。
Go 的標準庫的設計裏面帶有濃郁的 Unix 氣息。比起 Java 之類的語言,它的庫代碼有不少不方便的地方。有時候引入了一些函數式語言的方式,但卻因爲 Unix 思惟的限制,不但沒能發揮函數式語言的優勢,並且致使了不少理解的複雜性。
一個例子就是 Go 處理字符串的方式。在 Java 裏每一個字符串裏包含的字符,缺省都是 Unicode 的「code point」。然而在 Go 裏面 string 類型裏面每一個元素都是一個 byte,因此每次你都得把它 cast 成「rune」類型才能正確的遍歷每一個字符,而後 cast 回去。這種把任何東西都當作 byte 的方式,就是 Unix 的思惟方式,它引發過分底層和複雜的代碼。
我使用過 Go 的 template library 來生成一些網頁。這是一種「基本可用」的模板方式,然而比起不少其餘成熟的技術,倒是至關的不足的。讓我比較驚訝的是,Go 的 template 裏面夾帶的代碼,竟然不是 Go 語言本身,而是一種表達能力至關弱的語言,有點像一種退化的 Lisp,只不過把括號換成了 { {...} }
這樣的東西。
好比你能夠寫這樣的網頁模板:
{ {define "Contents"} }
{ {if .Paragraph.Length} }
<p>{ {.Paragraph.Content} }</p>
{ {end} }
{ {end} }
因爲每一個模板接受一個 struct 做爲填充的數據,你可使用 .Paragraph.Content
這樣的代碼,然而這不但很醜陋,並且讓模板不靈活,很差理解。你須要把須要的數據全都放進同一個結構才能從模板裏面訪問它們。
任何超過一行的代碼,雖然也許這語言能夠表達,通常人爲了不這語言的弱點,仍是在 .go 文件裏面寫一些「幫助函數」。用它們產生數據放進結構,而後傳給模板,纔可以表達模板須要的一些信息。而這每一個幫助函數又須要必定的「註冊」信息才能被模板庫找到。因此這些複雜性加起來,使得 Go 的 HTML 模板代碼至關的麻煩和混亂。
據說有人在作一個新的 HTML 模板系統,能夠支持直接的 Go 代碼嵌入。這些工做剛剛起步,並且難說最後會作成什麼樣子。因此要作網站,恐怕仍是最好使用其餘語言比較成熟的框架。
優雅和簡單性都是相對而言的。雖然 Go 語言在不少方面超過了 C 和 C++,也在某些方面好於 Java,然而它實際上是無法和 Python 的優雅性相比的,而 Python 在不少方面卻又不如 Scheme 和 Haskell。因此總而言之,Go 的簡單性和優雅程度屬於中等偏下。
因爲沒有明顯的優點,卻又有各類其它語言裏沒有的問題,因此在實際工程中,我目前更傾向於使用 Java 這樣的語言。我不以爲 Go 語言和它的工具鏈可以幫助我迅速的寫出 PySonar 那樣精密的代碼。另外我還據說有人使用 Java 來實現大併發,並沒發現比起 Go 有什麼明顯的不足。
Alan Perlis 說,語言設計不該該是把功能堆積起來,而應該努力地減小弱點。從這種角度來看,Go 語言引入了一兩個新的功能,同時又引入了至關多的弱點。
Go 也許暫時在某些個別的狀況有特殊的強項,能夠單獨用於優化系統的某些部分,但我不推薦使用 Go 來實現複雜的算法和整個的系統。