這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
本文分兩部分連載於2012年5月和6月的《程式員》雜誌。當時Go語言剛剛推出第一個穩定版:Go 1。刊載時略有刪改。
Go語言是Google於2009年推出的靜態編譯型語言,旨在為開發人員提供類似Python,Ruby一樣簡潔的語言環境,同時又具備C/C++一樣的運行效率。作為一個開源項目,在過去的兩年多時間裡,Go以社區協作的形式,不斷地完善語言和標準庫的設計與實現。最終於今年三月28日發布了第一個穩定的發行版本:Go version 1,簡稱Go 1。Go 1的推出,意味著Go語言和它的標準庫已經進入了一個穩定階段。對于謹慎的開發人員來說,開發Go程式正趁當下,現在已經可以放心地開發Go程式,不必再考慮未來文法和標準庫的變化。
正如Go官方所說,Go 1的目的是發行一個穩定的Go語言實現,而非全盤修改。所以對於已經熟悉Go的開發人員來說,Go 1與之前的版本並沒有很大差異。絕大部分修改都是對標準庫命名空間的再組織。本系列文章包含上下兩篇,上篇重點討論Go的開發背景,部分文法和類型系統;下篇討論Go的並行存取模型和工具鏈。
本文的全部代碼可以在github上找到。
編譯效率,運行效率,開發效率
曾經那些代碼,品讀起來,恰似滿腹經論的學者之間,細語輕聲,拂琴暢談。絕非烏煙瘴氣之下,面紅耳赤地與編譯器爭辯。--- Dick P. Gabriel
按照Go官方FAQ的說法,Go的出現是為了彌補其他語言在系統級開發上的缺陷。這樣一句話,難免有人會覺得,Go的誕生純粹是幾位電腦界大佬 --- Go最初的核心開發人員包括Robert Griesemer, Rob Pike, Ken Thompson --- 在埋怨自己的不肖後輩,並在吐槽同時,自己親自操刀開發了這樣一個語言。但更中肯地說法,恐怕是他們在目睹和體驗了Google的系統級開發之後,總結出的一套在異構,分布式多核系統之上的生存之道。而今天Google所面臨的問題,也許恰恰是幾年後每個公司的面試題目。與其說Go是一座空中樓閣,不如說是各位系統開發界大佬進入新時代後的一部略帶辛酸的開發史。
Go的基本設計理念是:編譯效率,運行效率和開發效率要三者兼顧。使用Go開發,要讓開發人員感覺到Python的便利,C/C++的運行效率,以及小到可以被忽略的編譯時間。為了實現這個理念,形成了Go語言的以下幾個特性:
記憶體回收,去除複雜的記憶體釋放工作。
簡潔的符號和文法,極力減少開發人員輸入的字元數。
平坦的類型系統,去除了複雜的繼承關係。使用結構化類型系統(Structural type system),既簡化了事前設計工作,也為未來增加抽象層提供了非侵入式的解決方案。
基於CSP模型的並行,簡化了並髮結構之間的通訊和資料共用。為多核時代的程式開發打好基礎。
比線程更輕量的goroutine,讓一個線程可以執行多個並髮結構。不必使用非同步通訊,就足以達到線程池與select/poll/epoll的效果。極大簡化了多串連的開發。
使用一套簡單的規範,開發人員不必再單獨編寫指令碼指定依賴關係和編譯流程。僅僅使用代碼本身和go工具鏈,就可以處理各種依賴關係。寫完代碼,一條命令,自動下來各種依賴,直接編譯/安裝。無需make,autoconf,automake,setup.py等工具支援。
前兩點應該是不言自明的。本系列文章重點對後五點做詳細分析。本篇後半部分將討論文法和類型系統;下篇將討論並行存取模型和工具鏈。關於如何提高編譯效率,由於涉及較多編譯器實現細節,在此不討論。
化繁為簡,文法當先
public static <I, O> ListenableFuture<O> chain (ListenableFuture<I>input, Function<? super I, ? extends ListenableFuture<? extends O>>function) 蒼天啊!大地啊!快把這貨攔下來吧!--- 來自某聊天記錄
初學Go,會讓人感到它神似C語言,並非是其背後強大的Team Dev和他們與C語言千絲萬縷的聯絡,也不僅僅是Go對系統級開發的重視和它類C的文法。而是簡潔與實用並存的文法讓人觸目難忘。在21世紀,一個嚴肅的通用程式設計語言,使用一份僅有約兩萬詞的語言規範,只定義25個關鍵字,42個操作符,卻涵蓋了並發,物件導向等方方面面,僅僅這些,對於開發人員來說,這個語言怕也足以值得一試了。
這裡列舉部分文法:
沒有分號了。其實Go語言規範中,Go中的語句的確是以分號做分割的。但Go語言規定,詞法分析器使用一套規則自動地添加分號。這種對詞法分析器的要求,帶來了一條編碼規範:左大括弧不要單獨寫在一行,否則詞法分析器可能會產生不必要的分號。雖然多了這樣一條編碼規範,但是帶來的結果是開發人員確實不必考慮分號了。以Go標準庫為例,其中沒有任何一個語句,需要開發人員明確地寫下分號。
使用:=操作符聲明變數和其初始值,不必明確指明變數類型,因為初始值已經說明了變數的類型。試想要聲明一個結構/類。使用Java需要foo.Foo a = new foo.Foo;, 而Go則只需a := new(foo.Foo)。對於比較長的類型名來說,這可以減少很多打字。需要注意的一點是,:=操作符同時完成了變數聲明和賦值的操作。如果變數之前已經聲明,只是要給它賦值,則依然使用常見的=賦值。
for,if的判斷條件,和switch的控制語句無需括弧。也就是說,可以寫成if a < b { dosomething() }。還可以寫switch b { case 1: dosomething() }。
所有迴圈只有for一個關鍵字。而for迴圈有可以有幾種寫法: 和C語言for迴圈類似的寫法for i := 0; i < 10; i++ { dosomething() };和C語言while迴圈類似的寫法for i < 100 { dosomething() };以及無條件迴圈for { iteration() };另外,對於切片類型(類似於Java中的數組,是一種線性結構)和map類型(go中的雜湊表實現),還可以配合range關鍵字,遍曆儲存的成員,如for i, e := range list { dosomething(i, e) }。
直觀簡單的存取控制。只有名字以大寫字母開頭的變數,函數/方法,或結構,才能被包(package)外代碼訪問。否則只是包內可見。這不僅簡化了存取控制,而且對開發人員來說,只需要看到名字,不必去找聲明,就可以知道訪問條件。
switch的條件可以是運算式且可以沒有控制語句。這意味著以下語句是合法的:
switch {
case i % 2 == 0:
process_even(i)
case i % 2 != 0:
process_odd(i)
}
這些看似不經意的改變,卻有效簡化了程式的書寫和閱讀。讓開發人員減少打字之外,也讓讀程式變得更加簡單。由於這些改變和特性並不複雜,在此不多加介紹。
正交原則:資料,方法,複用和抽象
早知[C++]如此[複雜],要是能穿越回去,我們肯定會搞一個物件導向版本的C語言出來。--- Ken Thompson,UNIX創始人,Go語言作者,在接受採訪時關於Go和物件導向。
這一節主要討論Go的類型系統。和C語言一樣,Go也使用結構體進行資料抽象:
type Duck struct {
Name string
}
接下來,可以為這個結構定義一個方法:
func (d *Duck) Eat() {
fmt.Println(d.Name,
"Duck is having dinner!")
}
與C++,Java的類不同,為結構體添加方法不必在聲明結構體的時候就聲明該方法。只需要保證方法與結構體定義在同一個包(package)中。由此資料與行為被分離,在設計資料抽象(結構體)階段,不必考慮具體哪些行為。func關鍵字後面的(d *Duck)表示這個方法從屬於哪個類型。d這個變數被成為“接收者”(receiver),在方法的定義中,d的使用類似C++/Java中的this指標。
然後,我們可以使用這個結構體和它的方法了:
func main() {
d := new(Duck)
d.Name = "Donald"
d.Eat()
}
按照講述物件導向語言的規律,下一步應該介紹繼承機制了。Go的回答很簡單:沒有繼承。這聽起來似乎不可理喻,但卻帶來了直接的好處:極大地簡化了類型系統。一方面,編譯器可以更高效了;另一方面,開發人員不必事前考慮各種複雜的繼承關係。
但是,繼承的好處也非常明顯的:第一,複用,子類可以直接繼承父類的方法的實現,減少了重複代碼;第二,多態,開發人員可以使用更抽象的表示(父類)調用具體的實現(子類),由此可以編寫通用的代碼在抽象層次上進行操作。
Go如果只是粗魯地去掉繼承機制,而不去面對繼承所要解決的問題,顯然是不明智的。為此,Go分別使用兩套機制來實現繼承要達到的效果:匿名成員實現代碼複用;介面類型實作類別型抽象。
實現代碼複用,Go在結構體定義中,引入了“匿名成員”(Anonymous Field)的概念。瞭解物件導向設計的開發人員一定聽說過,要優先使用對象組合而非繼承的方式來實現代碼複用的原則。Go的匿名成員,實際也就是一種對象組合。
繼續上面的例子,假如需要定義一個DonaldDuck類型,它的Eat方法實現和Duck一樣,但是多了一個叫做Age的成員:
type DonaldDuck struct {
Duck
Age int
}
可以看到,僅僅是把Duck這個類型名字放在結構體的定義中。由於並沒有顯示地給出這個成員的名字,由此得名“匿名成員”。這個寫法本質上是定義了另外的一個結構體,它包含了一個類型為Duck,名字也叫做Duck成員(是的,這個成員名和類型名是一樣的)。同時,也包含了一個類型為整數,名為Age的成員。
那麼這匿名成員究竟對代碼複用有什麼意義呢?Go對匿名成員有一條特殊規則:包含匿名成員的結構體也具有了匿名成員類型的方法。簡單說,對於上面的例子,DonaldDuck中包含了匿名成員Duck,那麼就好像DonaldDuck實現了Duck的各種方法。這樣,下面的代碼就容易理解了:
func main() {
d := new(DonaldDuck)
d.Name = "Donald"
d.Age = 10
d.Eat()
}
我們首先需要申請一個DonaldDuck類型的對象,然後對這個對象的各個成員進行賦值,最後調用Eat()方法。至今為止,我們看到,使用匿名成員可以實現像繼承一樣的代碼複用。但是比起繼承,它又少了點東西。比如,你無法將一個DonaldDuck類型的對象地址賦值給一個Duck類型的指標。你也不能在DonaldDuck中重新定義Eat方法的實現。簡單說來,匿名成員僅僅在文法層面上做了一些簡化,並沒有觸及任何類型系統的內容。對於類型系統來說,DonaldDuck和Duck完全是兩個不同類型。
為了實現繼承機制的另外一部分,即多態,Go引入了介面類型的概念。與Java中的介面類似,Go的介面也是聲明了一組方法的原型,然後由具體結構體(類)的方法來實現各種介面。但是與Java不同的是,Go使用了類似OCaml的結構化類型系統(Structural Type System),這種類型系統不要求實現介面的類型顯示地聲明究竟要實現哪些介面。只需要定義好與介面類型一致的全部方法,就說該類型實現了這個介面。具體說來,對於以上代碼,我們可以定義一個Animal的介面:
type Animal interface {
Eat()
}
這個介面中僅僅定義了一個方法:Eat,它沒有任何輸入參數,也沒有傳回值。至此為止,Duck和DonaldDuck都是Animal這個介面的實現。沒錯,你不必修改Duck和DonaldDuck的代碼,不必顯示的寫明implements Animal,只要實現了Eat方法,並且原型與介面類型中定義的一致就可以了。那麼,我們就可以直接聲明一個Duck類型的指標,然後把它賦值給Animal介面類型的變數:
func main() {
var a Animal
d := new(Duck)
d.Name = "Don"
a = d
a.Eat()
}
如果還要哪些類型實現Animal這個介面,只需要為這些類型實現Eat方法,就可以了。
這種介面類型為開發人員提供了方便的“先實現,後抽象”的機制。因為介面本身是非侵入式的,不必在定義結構的時候就明確說明要實現哪些介面。開發人員可以在實現了一些結構之後,再尋找它們之間通用的介面表示形式,進而定義一個介面類型。這時候,任何實現了介面中指定方法的類型,都已經是這個介面的實現了。不必再修改以前運行良好的代碼,也極大地簡化了開發流程。試想,在幾萬行代碼中,尋找幾個實現了某些方法的類型,哪怕只在每個類型的定義中添加兩個單詞,也足以讓人生厭了。
這種“先實現,後抽象”的機制也符合我們認識世界的一般規律。哪怕是自己寫的代碼,往往也是在使用一段時間後,才發現它們背後的一些抽象表示。單純地要求在代碼開發之前的設計階段就定義好良好的類型層次,是一種無視人類認識局限的荒謬做法。Go作為一種實用的語言,承認這種局限的同時,為人們提供了最小修改代價的方案。
Go的類型系統體現了Go的另一個設計原則:正交原則。線性代數中,正交意味著一組向量之間沒有任何一個可以投影到其他向量上。引申到程式設計領域,就是任何特性只針對一個問題,並且互相之間沒有交集。具體到Go的類型系統,則體現在方法與資料的分離;和複用與抽象的分離。這樣的分離使得開發人員可以通過分析問題本身,有效地針對問題選擇不同特性。
小結
本篇重點討論了Go語言的文法和類型系統。強調了Go的一些基本設計原則。這一切都是以簡潔為基礎,試圖以最小的語言特性覆蓋各種常見問題,這酷似當年的C語言。儘管這樣的設計也許不會迎來多少學術界的讚譽,卻足以把開發人員從複雜的文法設計,層層的類型關係中拯救出來,用更清晰簡單的方法解決手頭的問題。
但只有這些還不足以令Go續寫C語言的輝煌。它必須要正視它所處時代必須面對的問題,這就是多核帶來的挑戰。如今的程式,不能再指望僅僅提高在單核上的運行效率,而提高整個程式的效率。面對多核時代,開發人員必須要發掘程式中潛在的並行結構,最大化利用多核的並行能力。而Go則為開發人員提供好了稱手工具,去迎接多核帶來的新挑戰。下期我們將重點討論Go語言中對於並行結構的處理,與go工具鏈。敬請關注。
--------------------------------------------------------------------------------------
歡迎關注碼術!