這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
繼續前文的翻譯。更進一步的對 Go 進行了介紹。一個德國人,用英文寫了如此的長篇大論,這是一種什麼樣的國際主義精神……
———————–翻譯分割線———————–
Go程式設計語言,或者:為什麼除了它,其他類C語言都是垃圾
[翻譯]Go程式設計語言,或者:為什麼除了它,其他類C語言都是垃圾(1)
[翻譯]Go程式設計語言,或者:為什麼除了它,其他類C語言都是垃圾(2)
擴充
Go 真正強大的在於到現在為止,那些無法在 C、C++或者其他上面提到的任何語言中找到對應的地方。這些才是真正讓 Go 光彩奪目的:
基於類型的對象 vs. 封裝
沒有類。類型和類型上的方法相互並無依賴。可以在任何類型上定義方法,也可以定義任何類型為一個新的類型,這和 C 中的 typedef 相似。與 C 或 C++ 的不同之處在於新的命名的類型有其自己的方法集合,而主要類型(那些作為基礎的)的也會有方法集合:
type Handle int64func (this Handle) String() string { return "This is a handle object that cannot be represented as String."}var global Handle
在這個例子中,global.String() 可以被調用。更進一步,我們獲得了一個沒有虛方法的對象系統。這沒有任何運行時的麻煩,只是文法糖而已。
鴨子類型(譯註:參考維基百科) vs. 多態
類型定義不能讓兩個獨立的類型看起來相似。獨立的類型就是獨立的類型,在類型嚴格的語言中不允許建立一個通用類型實現多態。在大多數語言中有一個很流行的例子,就是值轉換表達它的字串的規則。Handle 類型用 Go 的規則定義了這樣一個方法,但是沒有展示如何讓任意的類型都有這樣一個 String 方法。
C++ 使用介面(可能利用虛基類)並且重載運算子來實現這個。Java 的 toString 是根類的一部分,因此會被繼承,而其他調用規則都是根據介面來表達的。
Go 使用介面有些特別。不像 Java,它無須定義給定的類型匹配某個介面。如果可行,那麼就自動作為那個介面:
type Stringer interface { String() string}
這就是所有需要的。自動的,Handle 對象現在可以作為 Stringer 對象使用。如果它走起來像鴨子,叫起來像鴨子,並且看起來像鴨子,那麼從任何實際用途出發,它就是鴨子。現在最棒的部分:它可以動態工作。無須匯入,甚至開發人員無須知道介面的定義。
當類型作為介面使用時,運行時環境為了得到介面的運行時的反射的能力,構建了一個函數指標的表格。這樣就會有一些運行時的開銷。然而這進行了最佳化,所以只有很小的損失。介面表格只在真正使用的時候才進行計算,對於每個類型來說只計算一次。如果編譯時間能夠確定實際的類型,就完全避免在運行時處理。方法調度應當比 Apple(已經相當酷了)的 Objective C 的調度略快。
類型嵌入 vs. 繼承
類型的作用跟類類似,但是由於沒有繼承的層級,它們實際上是不同的。在之前的例子中,Handle 並沒有從 int64 中繼承任何方法。可以通過在聲明體中包含基礎資料類型,來定義一個類型結構實作類別似繼承的東西:(無恥的從“Effective Go”中竊取的例子)
type ReadWriter struct { *bufio.Reader *bufio.Writer}
bufio.Reader 和 bufio.Writer 有的所有方法這個類型都有。衝突用一個很簡單的規則解決。畢竟這不是多重繼承!每個基礎類型都作為等位型別中的一個獨立的資料對象存在,而來自子類型的每個方法只能看見它所擁有的對象。這個方法,就可獲得良好實用的行為,而不存在類的多重繼承導致的那些麻煩。拋開所有概念——這多多少少又是一個文法糖,無須運行時開銷就讓代碼更加有表現力。
這同樣工作於介面。等位型別匹配全部組成類型匹配的介面。解決引用的方法歧義的規則非常簡單,而無法預測的情況被簡單的禁止了。等位型別可以自由的按照需要重寫組成的方法。
可見控制
開發中的主要單元是包。一個或者多個檔案實現一個包,並且可以控制從包外訪問的可見情況。這裡沒有複雜的可見系統,僅僅是可見或不可見。這是由排版規則控制的:公用名字由大寫字母開始,私人名字用小寫字母。這對於所有命名都有效。
這是個相當務實的方案。不像類型系統,這在競爭中相當有力,這裡 Go 佔領了中土:多數指令碼語言完全不關心可見度或者依賴自覺規定,而學校式的 C 系語言有存取控制的細節。同樣,這對於 Go 的物件模型也是一件好事。由於沒有類繼承並且內嵌物件是完全結合的,就沒有需要有訪問保護規則。下面會看到這是如何在實踐中工作的。
沒有建構函式
對象系統沒有特別的建構函式。這裡有一個零值的概念,例如用零初始化類型的所有欄位。這要求在編寫代碼時,零值對於合法的“空”對象處理是有意義的。如果無法做到,就提供作為包函數的建構函式。
Goroutine 和 Channel
對於這種通用程式設計語言來說,這是最不尋常的功能。Goroutine 可以認為是極為輕量的一種線程。Go 運行時將這些映射為 pth 形式的多任務協作偽線程或真正的作業系統線程,前者擁有較低的開銷,後者通過線程得到了非阻塞的行為,因此得到了兩者的最優。
Channel 是有類型的緩衝或無緩衝的訊息佇列。一個簡單的實現,真的。從一邊裝填入對象,從另一邊取出來。你不得不在並行演算法中做的許多事情都不再需要了。
Go 讓 Goroutine 作為一等公民是一大進步,許多演算法都可以用其作為核心。同段的棧使得每個線程的最小棧使用都比較低,通過類比線程得到效能上的優勢,除非使用阻塞的系統調用,也就是僅比普通函數調用的開銷略多一點。
綜上所述,Go 有真正的終極武器,Goroutine 可以用在許多地方,而不是標準的並發演算法。Python 的 generator 函數可以作為模型,或者一個自訂的自由對象記憶體管理表。參閱線上文檔,如此簡單的並發實現是一件神奇的事情。
順便說一下,通過設定環境變數,你可以告訴 Go 運行時希望使用多少個 CPU,這樣 Goroutine 將會在開始的時候映射若干個原生線程(預設如果沒有阻塞的系統調用,就不開啟任何其他線程)。
缺陷
沒有系統是完美的。Go 有一些缺陷,這裡的列表列出了現在我遇到的:
二進位大小/運行時依賴
基本的 Go 二進位是靜態連結,如果編譯不包括調試資訊,大約 750k。這和類似的 C 程式大小相同。我已經用 Go 首頁上的“比較樹”例子進行了測試,將其同類似結構的我的 C 實現進行了比較。
gccgo 可以編譯動態連結的執行檔案,但是 libc 在所有系統上,並且通常不需要考慮依賴,而 libgo 有大約 8MB 的額外的包。作為比較:libstdc++ 小於 1 MB,libc 小於 2MB。公平的說,它們相比 Go 的標準庫要少做很多工作。然而,還是有很大差異以及有依賴問題。
6g/8g,原生的 Go 編譯器,產生類似的執行檔案,但是並不依賴 libc,它們是真正獨立的。卻也無法實現運行時的動態串連。
這也同樣涉及小系統。在我的旁邊是古老的 16MB 奔騰-100 筆記本,運行著 X 和 JWM 案頭,正愉快的播放我的音樂收藏。它甚至有 5MB 的記憶體用於磁碟緩衝。有可能用 Go 編寫這樣的系統嗎?
不公平的權利
這個語言在許多方面存在特權。例如,特別的 make() 函數所做的工作,不能用使用者的代碼對其進行擴充。這在開始並不像看起來的那麼糟糕,例如可以寫一些與 make() 作用相同的代碼,僅僅是沒有辦法對這個語言的構造進行加強。同樣的問題在其他可能需要擴充的調用和保留字上都存在,例如 range。你或多或少被強制使用 goroutine 和 channel 擴充一個進階的出來。
我不確定這真得是個問題。假設 map、slice、goroutine 和 channel 是可選的實現,這個限制帶來的衝擊就不存在了。它並不損害代碼的清晰程度和可讀性,但是如果用過“可以模仿任何東西”的語言如 Perl 或者 Python,就會感覺有點不公平。
沒有重載
重載是許多語義歧義的根源。有很好的理由讓它滾蛋。但是,同時重載,尤其是運算子多載,是如此方便和可讀的,因此我很想念它。Go 沒有自動的類型轉換,因此事情不會像在 C++ 中那樣令人毛骨悚然。
作為例子,重載可能用於的地方,設想大數庫、(數字)向量、矩陣,或者有限範文的資料類型。當處理誇平台資料交換,可以對特殊資料類型修改數學語義,會是一個巨大的勝利。對於處理古董機的資料,可以有補充的數字類型,例如,用完全類比目標平台的運算,來代替依賴當前平台語義上的相同。
有限的鴨子類型
不幸的是,鴨子類型是不完全的。設想一個像這樣的介面:
type Arithmetic interface { Add(other int) Arithmetic}
函數 Add 的參數和傳回值將會限制自動的類型化。一個有方法 func (this MyObj) Add(other int) MyObj 的對象不匹配 Arithmetic。有許多類似這樣的例子,而它們中的一部分很難決定到底應該用鴨子類型覆蓋它們,或者當前的規則更好。你可能落入許多不太明顯的問題中,因此這是個“保持簡單可能會更好”的例子,但我總是覺得不夠方便。
Russ Cox,Go 核心的作者之一,說明:
這不能工作的原因是 MyOjb 的記憶體布局與 Arithmetic 的記憶體布局不同。即便是記憶體布局吻合的其他語言也在糾結於此。Go 只是說了“不”。
我猜測需要定義 func (this MyObj) Add(other int) Arithmetic 來代替。妥協的方案帶來的好處是編譯器和產生的機器碼更加簡單。
指標 vs. 值
我不確定對這個指標/值的事情到底高興不高興。Java 的所有都是引用的語義更簡單。C++ 引用 vs. 值的文法同樣也是不錯的。一個可能的方面是你獲得了更多對記憶體布局和使用結構的控制,特別是當包含其他結構時,而值 vs. 引用語義在函數調用的時候很清晰,C++ 卻難以預料。
順便說一下,map 和 slice 是參考型別。在最初的時候,我對它們感到煩惱,但是你可以構造有著類似行為的自己的對象:包含(私人)指標的結構體,這或多或少是 map 和 slice 做的事情。現在只剩下如果有辦法鉤掛到其 [...] 文法中去的話……
邏輯上下文(譯註:三元運算子,還記得嗎?)
邏輯上下文提供了許多簡化代碼的可能。不幸的是,雖然 !pointer 還是很清晰的,但指標仍然必須同 nil 相比較。更進一步說,從現在起不再有指標運算。參考上面的期望的第十條。在讓代碼工整同時有更短。
給每個類型一個零值的觀念,這微小的彌補了邏輯內容相關的不足。
———————–翻譯分割線———————–
發現德國佬普拉普拉的又修補了 review,同時加了一大堆內容。心都涼了,這個坑越挖越深了……