這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
Rob Pike談Google Go:並發,Type System,記憶體管理和GC
概要
Rob Pike談Google Go,內容涉及:無類OOP編程、Go的介面、採用Goroutines和Channels的並發特性,以及Go中協助縮短GC間歇的一些特性。
簡歷
Rob Pike是Google的首席工程師。最近,他參與開發了Go語言。在此之前,Rob在貝爾實驗室的CS研究中心工作,主要從事電腦圖形,語言,並發編程等方面的工作,同時他也是Plan 9及Inferno OSes的架構師。此外,他還與Brian Kernighan合著了《The Unix Programming Environment》及《The Practice of Programming》。
關於會議
GOTO會議的目標聽眾是軟體開發人員,IT架構師以及專案經理。GOTO Aarhus是在Denmark舉行的年度會議。之所以發起GOTO(以前稱為JAOO),是因為Trifork的管理部門不滿足於現有的一些會議,他們希望建立一個論壇,帶給開發人員學習的靈感、活力和渴望,同時也給大家帶來參與IT社區的機會。
Rob,你建立了Google Go這門語言。什麼是Google Go?能簡明扼要的介紹一下Google Go嗎?
我還是講講為什麼要建立這門語言吧,和你的問題稍有些不同。我在Google做了一個有關程式設計語言的系列講座,在Youtube上有,談及了我早期所寫的一個語言,叫做Newsqueak,那是八十年代的事,非常早。在做講座期間,我開始思考為什麼Newsqueak中的一些想法在我現在以C++為主的工作環境中無法使用。而且在Google我們經常要構建非常大的程式,光構建就要花很多時間,對依賴的管理也有問題,由於連結了本來並不需要的東西,二進位程式包變得很大,連結時間很長,編譯時間也很長,而且C++的工作方式有點古老,其底層實際上C,C++已經有三十年的曆史了,而C則更是有四十年了。用現今的硬體做計算,有很多新東西需要考慮:多核機器、網路化、分布式系統、雲端運算等等。
Go的主要特點是什嗎?有什麼重要功能?
對於大多數人來說,他們對Go的第一印象是該語言將並發性作為語言原語,這對我們處理分散式運算和多核這類東西來說非常好、也非常重要。我猜許多人會認為Go是一門簡單無趣的語言,沒有什麼特別的東西,因為其構想看起來一目瞭然。但實際上不能用第一印象來判斷Go。很多用過Go的人會發現它是一門非常高產而且有表現力的語言,能夠解決我們編寫這門語言時期望其所能解決的所有問題。
Go的編譯過程很快,二進位程式包又比較小,它管理依賴的方式如同管理語言本身的東西一樣。這裡還有一個故事呢,但是在這裡就不再展開討論了,但是這門語言的並發性使其能夠以非常簡單的模式來處理非常複雜的操作及分散式運算環境。我想最重要的功能可能就是並發性了,後面我們可以談談該語言的類型系統,其與C++、Java這類傳統物件導向類型系統的差異很大。
在我們繼續話題之前,能否解釋一下為什麼Go編譯器能達到那麼快的編譯速度呢?有什麼法寶?
它之所以快,有兩個原因。首先Go有兩個編譯器——兩個單獨的實現。一個是按照Plan 9(http://plan9.bell-labs.com/wiki/plan9/1/) 風格新寫的編譯器,它有自己獨特的工作方式,是個全新的編譯器。另一個編譯器叫做GCC Go,它擁有GCC前端,這個編譯器是Ian Taylor後來寫的。所以Go有兩個編譯器,速度快是二者的共同特點,但是Plan 9風格編譯器的速度是GCC Go的5倍,因為它從頭到腳都是全新的,沒有GCC後端,那些東西會花很多時間來產生真正的好代碼。
GCC Go編譯器要產生更好的代碼,所以速度慢些。不過真正重要的一點是Go編譯器的依賴管理特性才是其編譯速度快的真正原因。如果你去看一個C或C++程式,便會發現其標頭檔描述了函數庫、對象代碼等等東西。語言本身並不強制檢查依賴,每一次你都必須分析代碼以便清楚你的函數是怎樣的。如果你編譯過程中想用另一個類的C++程式,你必須先編譯它所依賴的類和標頭檔等等等等。如果你所編譯的C++程式有許多類,並且內部相關,你可能會把同一個標頭檔編譯數百次甚至上千次。當然,你可以用先行編譯標頭檔及其他技巧來迴避之一問題。
但是語言本身並不能幫上你的忙,工具可能會讓這一問題得到改善,可是最大的問題是並沒有什麼能保證你所編譯的東西就是程式真正需要的東西。有可能你的程式包含了一個並不真正需要的標頭檔,但是你沒辦法知道,因為語言並沒有強制檢查。而Go有一個更加嚴格的依賴模型,它有一些叫做包(packages)的東西,你可以把它想象成Java類檔案或著類似的東西,或者函數庫什麼的,雖然他們並不相同,但基本思路是一樣的。關鍵問題是,如果這個東西依賴那個東西,而那個東西又依賴另外一個東西,比如A依賴於B,B又依賴於C,那麼你必須首先編譯最內層的依賴:即,你先編譯C,然後編譯B,最後編譯A。
但是如果A依賴B,但是A並不直接依賴於C,而是存在依賴傳遞,那麼該怎麼辦呢?這時所有B需要從C拿到的資訊都會被放在B的對象代碼裡。這樣,當我編譯A的時候,我不需要再管C了。於是事情就非常簡單了:在你編譯器時,你只需將類型資訊沿著相依樹狀結構向上遍曆即可,如果你到達樹的頂端,則只需編譯緊鄰的依賴,而不用管其它層級的依賴了。如果你要做算術運算,你會發現在Objective-C或C++或類似的語言裡,雖然只包含了一個簡單的標頭檔,但由於依賴傳遞的存在,你可能會編譯數十萬行程式。然而在Go中,你開啟一個檔案,裡面或許只有20行,因為其中只描述了公用介面。
如果一個依賴鏈裡只有三個檔案,Go的優勢可能並不明顯,但是如果你有成千上萬個檔案的時候,Go的速度優勢會成指數增長。我們相信,如果用Go的話,我們應該能夠在數秒內就編譯完數百萬行代碼。然而如果是等量的用C++編寫的程式,由於依賴管理問題,編譯的開銷會大得多,編譯的時間將會長達若干分鐘。因此,Go速度快的根源主要歸功於對依賴的管理。
讓我們開始聊聊Go裡的類型系統吧。Go裡面有結構(struct)、有類型(type),那麼Go裡的類型是什嗎?
Go裡的類型與其它傳統程式設計語言裡的類型是類似的。Go裡的類型有整數、字串、struct資料結構、以及數組(array),我們稱之為切片(slice),它們類似於C的數組,但更便於使用,更加固定一些。你可以聲明本地類型並予以命名,然後按照通常的方式來使用。Go和物件導向方式的不同之處在於,類型只是書寫資料的一種方式,方法則是一個完全獨立的概念。你可以把方法放在struct上,在Go裡沒有類的概念,取而代之的是結構,以及為此結構聲明的一些方法。
結構不能與類混為一談。但是你也可以把方法放在數組、整數、浮點數或字串上,實際上任何類型都可以有方法。因此,這裡方法的概念比Java的方法更加泛化,在Java裡方法是類的一部分,僅此而已。例如,你的整數上可以有方法,聽上去似乎沒什麼用,但是如果你想在一個叫做Tuesday的整數常量上附加上to_string方法來列印出漂亮的星期格式;或者,你想重新格式化字串使其能夠以不同的方式列印出自己,這時你就會意識到它的作用。為什麼非要把所有方法或者其它好東西都塞進類裡面呢,為什麼不讓它們提供更廣泛的服務呢?
那麼這些方法只是在包內部可見嘍?
非也,實際上是這樣,Go只允許你在包內為你所實現的類型定義方法。我不能引入你的類型然後直接把我的方法增加進去,但是我可以使用匿名屬性(anonymous field)將其包裹起來,方法可不是你想加到哪就加到哪的,你要定義類型,然後才能把方法放在上面。正因為如此,我們在包裡提供了另一種封裝——介面(interface),但是如果你不明白誰能為對象增加方法的嚴格界限,就很難理解介面。
你的意思是,我可以給int增加方法,但是必須先使用typedef嗎?
你要typedef一個整數類型,起個名字,如果你正在處理一星期中的七天,可以就叫它“Day”,你可以給你所聲明的類型——Day增加方法,但是你不能直接給int增加方法。因為整數類型不是你定義的,不在你的包裡,它是引入的但並不在你的包中定義,這就意味著你不能給其增加方法。你不能給不在你包裡定義的類型增加方法。
你們借鑒了Ruby裡開放類的思想,這很有意思。Ruby的開放類實際上是可以修改類並增加新的方法,這是有破壞性的,但是你們的方法本質上是安全的,因為建立了新的東西。
它是安全可控的,而且很容易理解。最初我們覺得類型用起來可能不太方便,我們也希望像Ruby那樣添加方法,但這又讓介面比較難以理解。所以,我們只把方法取出來,而不是放進去,我們想不出有什麼更好的辦法,於是限制方法只能在本地類型上,不過這種思路確實很容易理解和使用。
你還提到了typedef,是叫typedef吧?
應該叫“type”,你所說的類型——Day的定義方式是這樣“type Day int”,這樣你就有一個新類型了,你可以在其上增加方法、聲明變數,但這個類型不同於int,不像C那樣,只是同一事物另起了個名字而已,在Go裡實際上你建立了一個不同於int的新類型,叫做“Day”,它擁有int的結構特性,但卻有自己的方法集。
Typedef在C裡是一種預先處理指令嗎?【編輯注/免責申明:C語言裡的typedef與預先處理無關】
那實際上就是個別名,但在Go裡不是別名,是新類型。
我們從底層說起吧,在Go裡最小的類型是什嗎?
最小的類型應該是布爾類型(bool)吧。bool、int和float,然後是int32、float64之類有尺寸的類型、字串、複雜類型,可能有遺漏,但這就是基本類型集了。你可以由這些類型構建結構、數組、映射(map),映射在Go裡是內建類型不是函數庫。然後我想就該是介面了,到了介面,有趣的東西才真正開始。
但是,int這樣的類型是實值型別對吧。
Int是實值型別。在Go裡,任何類型都是實值型別,和C一樣,所有東西都是按值調用,但是你也可以用指標。如果你想引用某樣東西,可以擷取其地址,這樣你就有了一個指標。Go也有指標但是比C指標有更多限制,Go裡的指標是安全的,因為他們是型別安全的,所以你沒法欺騙編譯器,而且也沒有指標運算,因此,如果你有個指向某物的指標,你無法將其移到對象外,也無法欺騙編譯器。
它們類似C++的引用嗎?
是的,很像引用,但是你可以按照你預期的方式對它們進行寫操作。而且你可以使用結構內部(如緩衝區)中間的某個地址,它和Java的引用不一樣。在Java中,你必須在旁邊分配一個緩衝區,這是額外的開銷。在Go中,你實際上把該對象分配為結構的一部分,在同一記憶體塊中,這對效能是非常重要的。
它是結構內部一個綜合物件。
是的,如果它是值而不是指標的話,是這樣。當然你也可以把指標放在結構內部和外部,但是如果你有struct A,而把struct B放在struct A裡,那麼stuct B就是一塊記憶體,而不像Java那樣,這也是Java效能問題的原因之一。
你提到過介面比較有趣,那下面咱們就談談這一部分。
Go裡的介面真的非常、非常地簡單。介面指明了兩個不同事情:其一,它表明了類型的構思,介面類型是一個羅列了一組方法的類型,因此如果你要抽象一組方法來定義一個行為,那麼就定義一個介面並聲明這些方法。現在你就有了一個類型,我們就叫它介面類型吧,那麼從現在起所有實現了介面中這些方法的類型——包括基本類型、結構、映射(map)或其它什麼類型,都隱含符合該介面要求。其二,也是真正有意思的是,和大多數語言中的介面不同的是,Go裡面沒有“implements”聲明。
你無須說明“我的對象實現了這個介面”,只要你定義了介面中的那些方法,它就自動實現了該介面。有些人對此感到非常擔憂,依我看他們想說的是:知道自己實現(Implement)了什麼介面真的很重要。如果你真想確定自己實現了什麼介面,還是有技巧可以做到這一點的。但是我們的想法與此截然不同,我們的想法是你不應該考慮實現什麼介面,而是應該寫下要做的東西,因為你不必事前就決定要實現哪個介面。可能後來你實際上實現了某個現在你尚不知曉的介面,因為該介面還未設計出來,但是現在你已經在實現它。
後來你可能發現兩個原先未曾考慮過相關性的類具有了相關性——我又用了類這個詞,我思考Java太多了——兩個structs都實現了一些非常有用的小子集中的相關方法,這時有辦法能夠操作這兩個structs中的任意一個就顯得非常有用了。這樣你就可以聲明一個介面,然後什麼都不用管了,即使這些方法是在別人的代碼中實現的也沒問題,雖然你不能編輯這些代碼。如果是Java,這些代碼必須要聲明實現你的介面,在某種意義上,實現是單向的。然而在Go裡,實現是雙向的。對於介面實際上有不少漂亮而簡單的例子。
我最愛用的一個真執行個體子就是“Reader”,Go裡有個包叫做IO,IO包裡有個Reader介面,它只有一個方法,該方法是read方法的標準聲明,比如從作業系統或檔案中讀取內容。這個介面可以被系統中任何做read系統調用的東西所實現。顯然,檔案、網路、緩衝、解壓器、解密機、管道,甚至任何想訪問資料的東西,都可以給其資料提供一個Reader介面,然後想從這些資源中讀取資料的任何程式都可以通過該介面達到目的。這有點像我們前面說過的Plan 9,但是用不同的方式泛化的。
與之類似,Writer也是比較好理解的另一個例子,Writer 由那些要做寫操作的人來實現。那麼在做格式化列印時,fpringf的第一參數不是file了,而是Writer。這樣,fprintf可以給任何實現了write方法的東西做IO格式化的工作。有很多很好的例子:比如HTTP,如果你正在實現一個HTTP伺服器,你僅須對connection做fprintf,便可將資料傳遞到用戶端,不需要任何花哨的操作。你可以通過壓縮器來進行寫操作,你可以通過我所提到的任何東西來進行寫操作:壓縮器、加密機、緩衝、網路連接、管道、檔案,你都可以通過fprintf直接操作,因為它們都實現了write方法,因此,隱含都隱含符合writer介面要求。
某種程度上有點類似結構化類型系統(structural typing,http://en.wikipedia.org/wiki/Structural_type_system)。
不考慮它的行為的話,它是有點像結構化類型系統。不過它是完全抽象的,其意並不在擁有什麼,而是能做什麼。有了結構(struct)之後,就規定了其記憶體的樣子,然後方法說明了結構的行為,再之後,介面則抽象了該結構及其它實現了相同方法的其他結構中的這些方法。這是一種鴨子類型系統(duck typing,一種動態類型系統, http://en.wikipedia.org/wiki/Duck_typing),而不是結構化類型系統。
你提到過類,但Go沒有類,對吧。
Go沒有類。
但是沒有類怎麼去寫代碼?
帶方法的結構(stuct)很像是類。比較有意思的不同之處是,Go沒有子類型繼承,你必須學習Go的另類寫法,Go有更強大、更有表現力的東西。不過Java程式員和C++程式員剛開始使用Go的時候會感到意外,因為他們實際上在用Go去編寫Java程式或C++程式,這樣的代碼工作得並不好,你可以這樣做,但這樣就略顯笨拙了。但是如果你退一步,對自己說“我該怎樣用Go去編寫這些東西呢?”,你會發現模式其實是不同的,用Go你可以用更短的程式來表達類似的想法,因為你不需要在所有子類裡重複實現行為。這是個非常不同的環境,比你第一眼看上去的還要不同。
如果我有一些行為要實現,而且想放在多個structs裡,怎麼去共用這些行為?
有一個叫做匿名域的概念,也就是所謂的嵌入。其工作方式是這樣:如果你有一個結構(struct),而又有一些其它東西實現了你想要的行為,你可以把這些東西嵌入到你的結構(struct)裡,這樣,這個結構(struct)不僅僅可以獲得被嵌入者的資料還可以獲得它的方法。如果你有一些公用行為,比如某些類型裡都有一個name方法,在Java裡的話你會認為這是一組子類(繼承來的方法),在Go裡,你只需拿到一個擁有name方法的類型,放在所有你要實現這個方法的結構裡,它們就會自動獲得name方法,而不用在每個結構裡都去寫這個方法。這是個很簡單的例子,但有不少有趣的結構化的東西使用到了嵌入。
而且,你還可以把多個東西嵌入到一個單一結構中,你可以把它想象成多重繼承,不過這會讓人更加迷惑,實際在Go裡它是很簡單的,它只是一個集合,你可以放任何東西在裡面,基本上聯合了所有的方法,對每個方法集合,你只需寫一行代碼就可以擁有其所有行為。
如果有多重繼承命名衝突的問題該怎麼辦?
命名衝突實際上並沒什麼,Go是靜態處理這一問題的。其規則是,如果有多層嵌入,則最高層優先;如果同一層有兩個相同的名字或相同的方法,Go會給出一個簡單的靜態錯誤。你不用自己檢查,只需留意這個錯誤即可。命名衝突是靜態檢查的,而且規則非常簡單,在實踐中命名衝突發生的也並不多。
因為系統中沒有根對象或根類,如果我想得到一個擁有不同類型的結構的列表,應該怎麼辦?
介面一個有意思的地方是他們只是集合,方法的集合,那麼就會有空集合,沒有任何方法的介面,我們稱之為空白介面。系統中任何東西都符合空介面的要求。空介面有點類似於Java的Object,不同之處在於,int、float和string也符合空介面,Go並不需要一個實際的類,因為Go裡沒有類的概念,所有東西都是統一的,這有點像void*,只不過void*是針對指標而不是值。
但是一個空介面值可以代表系統中的任何東西,非常具有普遍性。所以,如果建立一個空介面數組,實際上你就有了一個多態性容器,如果你想再把它拿出來,Go裡面有類型開關,你可以在解包的時候詢問裡面的類型,因此可以安全的進行解包操作。
Go裡有叫做Goroutines的東西,它們和coroutines有什麼區別?不一樣嗎?
Coroutines和Goroutines是不同的,它們的名字反應了這一點。我們給它起了個新名,因為有太多術語了,進程(processes)、線程(threads)、輕量級線程、弦(chords),這些東西有數不清的名字,而Goroutines也並不新鮮,同樣的概念在其它系統裡已經都有了。但是這個概念和前面那些名字有很大不同,我希望我們自己起名字來命名它們。Goroutine背後的含義是:它是一個coroutine,但是它在阻塞之後會轉移到其它coroutine,同一線程上的其它coroutines也會轉移,因此它們不會阻塞。
因此,從根本上講Goroutines是coroutines的一個分支,可在足夠多的操作線程上獲得多路特性,不會有Goroutines會被其他coroutine阻塞。如果它們只是協作的話,只需一個線程即可。但是如果有很多IO操作的話,就會有許多作業系統動作,也就會有許多許多線程。但是Goroutines還是非常廉價的,它們可以有數十萬之眾,總體運行良好並只佔用合理數量的記憶體,它們建立起來很廉價並有記憶體回收功能,一切都非常簡單。
你提到你們使用了m:n執行緒模式,即m個coroutines映射到n個線程上?
對的,但是coroutines的數量和線程的數量是按照程式所做工作動態決定的。
Goroutines有用於通訊的通道嗎?
是的,一旦有兩個獨立執行的功能,如果Goroutine們要相互協作它們就需要相互對話。所以就有了通道這個概念,它實際上是一個類型訊息佇列,你可以用它來發送值,如果你在Goroutine中持有通道的一端,那麼你可以發送類型值給另外一端,那一端則會得到想要的東西。通道有同步和非同步之分,我們儘可能使用同步通道,因為同步通道的構思非常好,你可以同時進行同步和通訊,所有東西運行起來都步調一致。
但是有時由於效率原因或調度原因,對訊息進行緩衝也是有意義的。你可以向通道發送整型訊息、字串、結構、指向結構的指標等任何東西,非常有意思的事,你可以在通道上發送另一個通道。這樣,我就能夠把與他人的通訊發送給你,這是非常有意思的概念。
你提到你們有緩衝的同步通道和非同步通道。
不對,同步是沒有緩衝的;非同步和緩衝是一個意思,因為有了緩衝,我才能把值放在緩衝的空間裡進行儲存。但是如果沒有緩衝,我必須等著別人把值拿走,因此無緩衝和同步是一個意思。
每個Goroutine就像是一個小的線程,可以這麼給讀者解釋吧。
對,但是輕量級的。
它們是輕量級的。但是每個線程同樣都預分配棧空間,因而它們非常耗費資,Goroutines是怎麼處理的呢?
沒錯,Goroutines在被建立的時候,只有非常小的一個棧——4K,可能有點小吧,這個棧是在堆中的,當然,你知道如果在C語言裡有這麼一個小棧會發生什麼,當你調用函數或分配數組之類的東西時,程式會馬上溢出。在Go裡則不會發生這樣的事情,每個函數的開頭都會有若干指令以檢查棧指標是否達到其界限,如果到達界限,它會連結到其它塊上,這種串連的棧叫做分段棧,如果你使用了比剛開始啟動時更多的棧,你就有了這種棧塊連結串,我們稱之為分段棧。
由於只有若干指令,這種機制非常廉價。當然,你可以分配多個棧塊,但是Go編譯器更傾向於將大的東西移到堆上,因此實際上典型的用法是,你必須在達到4K邊界之前調用幾個方法,雖然這並不經常發生。但是有一點很重要:它們建立起來很廉價,因為僅有一次記憶體配置,而且分配的記憶體非常小,在建立一個新的Goroutine時你不用指明棧的尺寸,這是很好的一種抽象,你根本不用擔心棧的大小問題。之後,棧會隨需求增長或縮小,你不用擔心遞迴會有問題,你也不用擔心大的緩衝或任何對程式員完全不可見的東西,一切由Go語言來打理,這是一門語言的整體構思。
我們再來談談自動化方面的東西,最初你們是將Go語言作為系統級語言來推廣的,一個有趣的選擇是使用了記憶體回收行程,但是它速度並不快或者說有記憶體回收間歇問題,如果用它寫一個作業系統的話,這是非常煩人的。你們是怎麼看這一問題的?
我認為這是個非常難的問題,我們也還沒有解決它,我們的記憶體回收行程可以工作,但是有一些延遲問題,記憶體回收行程可能會停頓,但是我們的看法是,我們相信儘管這是一個研究課題,雖還沒解決但是我們正在努力。對於現今的並行機,通過把機器核心的一些片段專門分給作為背景工作的記憶體回收來進行並行回收是可行的。在這一領域有很多工作要做,也取得了不少成功,但這是個很微妙的問題,我不認為而我們會把延遲降為0,但是我相信我們可以讓延遲儘可能低,這樣對於絕大多數系統軟體來講它不再是個問題。我不保證每個程式都不會有顯著延遲,但是我想我們可以獲得成功,而且這是Go語言中一個比較活躍的領域。
有沒有方法能夠避免直面記憶體回收行程,比如用一些大容量緩衝,我們可以把資料扔進去。
Go可以讓你深入到記憶體布局,你可以分配自己的空間,如果你想的話可以自己做記憶體管理。雖然沒有alloc和free方法,但是你可以聲明一個緩衝把東西放進去,這個技巧可用來避免產生不必要的垃圾。就像在C語言一樣,在C裡,如果你老是malloc和free,代價很大。因此,你分配一個對象數組並把它們連結在一起,形成一個鏈表,管理你自己的空間,而且還不用malloc和free,那麼速度會很快。你可以做與Go所做相同的事情,因為Go賦予你與底層事物安全打交道的能力,因此不用欺騙類型系統來達到目的,你實際上可以自己來做。
前面我表達了這樣的觀點,在Java裡,無論何時你在結構裡嵌入其它東西,都是通過指標來實現的,但在Go裡你可以把它放在一個單一結構中。因此如果你有一些需要若干緩衝的資料結構,你可以把緩衝放在結構的記憶體裡,這不僅意味著高效(因為你不用間接得到緩衝),而且還意味著單一結構可以在一步之內進行記憶體配置與記憶體回收。這樣開銷就會減少。因此,如果你考慮一下記憶體回收的實際情況,當你正在設計效能要求不高的東西時,你不應該總是考慮這個問題。但如果是高效能要求的,考慮到記憶體布局,儘管Go是具有真正記憶體回收特性的語言,它還是給了你工具,讓你自己來控制有多少記憶體和產生了的垃圾。我想這是很多人容易忽略的。
最後一個問題:Go是系統級語言還是應用級語言?
我們是把他設計為一種系統級語言,因為我們在Google所做的工作是系統級的,對吧?Web伺服器和資料庫系統、以及儲存系統等,這些都是系統。但不是作業系統,我不知道Go是否能成為一個好的作業系統語言,但是也不能說它不會成為這樣的語言。有趣的是由於我們設計語言時所採用的方法,Go最終成為了一個非常好的通用語言,這有點出乎我們意料。我想大多數使用者並沒有實際從系統觀點來考慮過它,儘管很多人做過一點Web伺服器或類似東西。
Go用來做很多應用類的東西也非常不錯,它將會有更好的函數庫,越來越多的工具以及一些Go更有用的東西,Go是一個非常好的通用語言,它是我用過的最高產的語言。
Rob Pike談論為什麼Google要開發新的Go語言
Rob Pike總是會在恰當的地方做事。在80年代初期,他在貝爾實驗室與Brian Kernighan以及Ken Thompson一起工作,與Kernighan合寫了“UNIX編程環境”,並與Thompson合作開發了UTF-8 位元組編碼通訊協定。Pike現在是Google的一個首席工程師,並與人合作開發了Go,一個新的程式設計語言。Pike將在下月的OSCON大會上討論Go,他也在下面的採訪中談論了Go的開發以及程式設計語言的目前狀態。
建立Go語言的動機是什嗎?
Rob Pike:幾年以前,我們在Google的幾個人開始對軟體開發過程感到沮喪,尤其是在使用C++編寫大型的伺服器軟體時更是如此。我們發現二進位檔案變得越來越大。他們需要花費太多的時間去編譯。此語言幾乎是當前世界上主要的系統軟體語言,其本身已經是一種非常老舊的語言了。最近二十年來許多在硬體上的思想和變化都未能影響C++。於是我們決定著手從新設計設計一種能解決我們問題的語言:我們需要快速地構建軟體,使其能在現代的多核心硬體和網路環境中運行良好,並且做到快樂編程。
儘管我們針對一些特定的問題來設定Go的目標,它已經變成一個比我們最初的設想更加通用和適用的程式設計語言。因此我們現在正使用它做許多不同的事情。我認為它應該在多個方向上都有美好的未來。
使用Go編程會感覺如何呢?
Rob Pike:Go具有動態語言如Python、Ruby或JavaScript的味兒,但它同時具有像Java、C或C++類語言的效能和安全性。這樣你會感覺就像在使用一個輕量級的現代的指令碼動態語言,但同時獲得了傳統語言的健壯性和效能。
Go是否具有一個強大的開發環境?
Rob Pike:我們有一套與此語言配合使用的有趣的工具。其中發布版本的標準庫就有完整性分析器。因此依據問題的複雜程度,你可以編寫你自己的工具,也許在使用已有庫時只需要一頁代碼。
有一些工具可以使你與已有的庫串連。對於大型的如OpenGL一類的包,你最好是僅僅串連使用已有的東西。我們可以使用我們的封裝工具來做這些事,另外SWIG支援使得我們能與C++串連。但所有的基底類別庫都是由Go編寫的。
已經有了針對Eclipse以及其他環境的外掛程式。但還需要更多的工作,我們尚沒有一個IDE,我們已經有了做這些事情的一些想法。
世界上還需要另外一個程式設計語言嗎?
Rob Pike:這是一個關於語言的愉快時光,因為已經產生了如此眾多的語言。60年代末到70年代初是語言開發的爆發期,接下來則一切歸於平靜。這並不是說沒有出現新的語言,而是語言的設計似乎都無功而返。然而在最近的5到10年,又一次出現了複興。我將在OSCON上談論這種現象的原因之一,便是當前常用的語言並不能滿足人們的需求。 這裡有新語言的生存之所,在這些方面Java、C、C++、JavaScript甚至Python都不能很好的滿足需求。
Google與貝爾實驗室比起來怎麼樣呢?
Rob Pike:這兩方面都已經發生了很多改變。當我在貝爾實驗室工作時,我們做了許多研究驅動的、出版導向的事情。在很大程度上,此公司不理解開源。當我到Google後,方向則變得非常不同。我們完全是一個試圖讓事情實現的公司。至少在後來,開源變成了公司文化的一個基本部分。因此在這方面他們是非常不同的。
隨著一天天的工作進展,我認為他們有許多共通點。他們都是令人嚮往的工作地點,他們都具有許多聰明的人們。但在文化上,他們是通訊公司和互連網公司的差別。他們從根本上是兩回事。
大道至簡
http://www.linuxeden.com/html/softuse/20120702/126645.html
這是我(Rob Pike)在 2012 年六月,舊金山 Go 會議上的演講內容。
這是一個私人演講。我並未代表 Go 項目團隊的任何人在此演講,但我首先要感謝團隊為 Go 的誕生和發展所做的一切。同時,我也要感謝舊金山 Go 社區給我這個演講機會。
在幾個星期之前我被問到,“在推出 Go 之後,什麼令你感到最為驚奇?”我立刻有了一個答案:儘管我們希望 C++ 程式員來瞭解 Go 並作為一個可選的語言,但是更多的 Go 程式員來自如於 Python、Ruby。只有很少來自 C++。
我們——Ken,Robert 和我自己曾經是 C++ 程式員,我們設計新的語言是為瞭解決那些我們編寫的軟體中遇到的問題。而這些問題,其他 C++ 程式員似乎並不怎麼在意,這看起來有些矛盾。
今天我想要談談是什麼促使我們建立了 Go,以及為什麼本不應該是這樣的結果會我們驚訝。我承諾討論 Go 會比討論 C++ 多,即便你不瞭解 C++ 也仍然完全跟得上主題。
答案可以概括為:你認為少既是多,還是少就是少?
這裡有一個真實的故事作為隱喻。貝爾實驗室最初用三個數位識別碼:111 表示物理研究,127 表示電腦科學研究,等等。在上世紀八十年代早期,一篇如期而至的備忘錄聲明由於我們所瞭解的研究正在增長,為了便於識別我們的工作,必須再添加一位元。 因此,我們的中心變為 1127。Ron Hardin 開玩笑半認真的說道,如果我們真的更好的瞭解了這個世界,我們可以減少一位元,使得 127 僅為 27。當然管理層沒有聽到這個笑話,又或者他們不願意聽到,但是我想這其中確有大的智慧。少既是多。你理解得越好,越含蓄。
請務必記住這個思路。
回到 2007 年 9 月,我在一個巨大的 Google C++ 程式(就是你們都用過的那個)上做一些瑣碎但是很核心的工作,我在那個巨大的分布式叢集上需要花大約 45 分鐘進行編譯。收到一個通知說 Google 僱傭的一對為 C++ 標準化委員會工作的夫婦將會做一場報告。收到一個通知說幾個受雇於 Google 的為 C++ 標準化委員會工作的人將會做一場報告。他們將向我們介紹那時還被稱作 C++0x(就是現在眾所周知的 C++11)中將會有哪些改進。
在長達一個小時的報告中,我們聽說了諸如有已經在計劃中的 35 個特性之類的事情。事實上有更多,但僅有 35 個特性在報告中進行了描述。當然一些特性很小,但是意義重大,值得在報告中提出。一些非常微妙和難以理解,如左右值引用(rvalue references),還有一些是 C++ 特有的,如可變參數模板(variadic templates),還有一些就是發瘋,如使用者定義資料標識(user-defined literals)。
這時我問了自己一個問題:C++ 委員會真得相信 C++ 的問題在於沒有足夠的特性?肯定的說,在另一個 Ron Hardin 的玩笑中,簡化語言的成就遠遠大於添加功能。當然這有點可笑,不過請務必記住這個思路。
就在這個 C++ 報告會的數月前,我自己也進行了一場演講,你可以在 YouTube 上看到,關於我在上世紀 80 年代開發的一個玩具性質的並發語言。這個語言被叫做 Newsqueak,它是 Go 的前輩了。
我進行這次報告是因為在 Newsqueak 中缺失的一些想法,在為 Google 工作的時候我再次思考了這些它們。我當時確信它們可以使得編寫服務端代碼變得更加輕鬆,使得 Google 能從中獲得收益。
事實上我曾嘗試在 C++ 中實現這些思路,但是失敗了。要將 C++ 控制結構和並行作業聯絡起來太困難了,最終這導致很難看到真正的優勢。雖然我承認我從未真正熟練的使用 C++,但是純粹的 C++ 仍然讓所有事情看起來過於笨重。所以我放棄了這個想法。
但是那場 C++0x 報告讓我再次思考這個問題。有一件令我十分困擾的事情(同時我相信也在困擾著 Ken 和 Robert)是新的 C++ 記憶體模型有原子類型。感覺上在一個已經負擔過重的類型系統上加入如此微觀的描述細節的集合是絕對的錯誤。這同樣是目光短淺的,幾乎能確信硬體在接下來的十 年中將迅速發展,將語言和當今的硬體結合的過於緊密是非常愚蠢的。
在報告後我們回到了辦公室。我啟動了另一個編譯,將椅子轉向 Robert,然後開始溝通關鍵的問題。在編譯結束前,我們已經把 Ken 拉了進來,並且決定做些什麼。我們不準備繼續寫 C++ 了,並且我們——尤其是我,希望在寫 Google 代碼的時候能夠做輕鬆的編寫並發。同時我們也想勇往直前的駕馭“大編程”,後面會談到。
我們在白板上寫了一堆想要的東西,和其必要條件。忽略了文法和語義細節,設想了藍圖和全域。
我這裡還有那時的一個令人神混魂顛倒的郵件。這裡摘錄了一部分:
Robert: 起點:C,修複一些明顯的缺陷,移除雜物,添加一些缺失的特性。
Rob: 命名:“go”。你們可以編造這個名字的來由,不過它有很好的底子。它很短,容易拼字。工具:goc, gol, goa。如果有互動式調試器/解譯器,可以就叫做“go”。副檔名是 .go。
Robert 空介面:interface {}。它們實現了所有的介面,所以這個可以用來代替 void *。
我們並沒有正確描繪全部的東西。例如,描繪 array 和 slice 用了差不多一年的時間。但是這個語言特色的大多數重要的東西都在開始的幾天裡確定下來。
注意 Robert 說 C 是起點,而不是 C++。我不確定,不過我相信他是指 C,尤其是 Ken 在的情況下。不過事實是,最終我們沒有從 C 作為起點。我們從頭開始,僅僅借鑒了如運算子、括弧、大括弧、和部分關鍵字。(當然也從我們知道的其他語言中吸取了精髓。) 無論如何,我們現在同 C++ 做著相反的事情,解構全部,回到原點重新開始。我們並未嘗試去設計一個更好的 C++,甚至更好的 C。僅僅是一個對於我們在意的那種類型的軟體來說更好的語言。
最終,它成為了一個與 C 和 C++ 完全不同的語言。每個發布版本都越來越不同。我製作了一個 Go 中對 C 和 C++ 進行的重要簡化的清單:
規範的文法(無需用於解析的符號表)
垃圾收集(唯一)
沒有標頭檔
明確依賴
無循環相依性
常量只能為數字
int 和 int32 是不同的類型
字母大小寫設定可見度
任何類型都可以有方法(沒有類)
沒有子類型繼承(沒有子類)
包層級初始化和定義好的初始化順序
檔案編譯到一個包中
包層級的全域表達與順序無關
沒有算數轉換(常量做了輔助處理)
隱式的介面實現(無需“implements”定義)
嵌入(沒有向父類的升級)
方法如同函數一樣進行定義(沒有特的別位置要求)
方法就是函數
介面僅僅包含方法(沒有資料)
方法僅通過名字匹配(而不是通過類型)
沒有構造或者析構方法
後自增和後自減是語句,而不是運算式
沒有前自增或前自減
賦值不是運算式
按照賦值、函數調用定義時的順序執行(沒有“sequence point”)
沒有指標運算
記憶體總是零值初始化
對局部變數取地址合法
方法沒有“this”
分段的堆棧
沒有靜態或其他類型註解
沒有模板
沒有異常
內建 string、slice、map
數組邊界檢查
除了這個簡化清單和一些未提及的瑣碎內容,我相信,Go 相比 C 或者 C++ 是更加有表達力的。少既是多。
但是即便這樣也不能丟掉所有東西。仍然需要構建類型工作的方式,在實踐中恰當的文法,以及讓庫的互動更好這種令人感到忌諱不可言喻的事情。
我們也添加了一些 C 或者 C++ 沒有的東西,例如 slice 和 map,複合聲明,每個檔案的頂級運算式(一個差點被忘記的重要東西),反射,垃圾收集,等等。當然,還有並發。
當然明顯缺少的是類型層次化。請允許我對此爆那麼幾句粗口。
在 Go 最初的版本中,有人告訴我他無法想像用一個沒有範型的語言來工作。就像之前在某些地方提到過的,我認為這絕對是神奇的評論。
公平的說,他可能正在用其自己的方式來表達非常喜歡 STL 在 C++ 中為他做的事情。在辯論的前提下,讓我們先相信他的觀點。
他說編寫像 int 列表或 map string 這樣的容器是一個無法忍受的負擔。我覺得這是個神奇的觀點。即便是那些沒有範型的語言,我也只會花費很少的時間在這些問題上。
但是更重要的是,他說類型是放下這些負擔的解決途徑。類型。不是函數多態,不是語言基礎,或者其他協助,僅僅用類型。
這就是卡住我的細節問題。
從 C++ 和 Java 轉過來 Go 的程式員懷念工作在類型上的編程方式,尤其是繼承和子類,以及所有相關的內容。可能對於類型來說,我是門外漢,不過我真得從未發現這個模型十分具有表達力。
我已故的朋友 Alain Fournier 有一次告訴我說他認為學術的最低級形式就是分類。那麼你知道嗎?類型層次化就是分類。你必須對哪塊進哪個盒子作出決策,包括每個類型的父級,不論是 A 繼承自 B,還是 B 繼承自 A。一個可排序的數組是一個排序過的數組還是一個數組表達的排序器?如果你堅信所有問題都是由類型驅動設計的,那麼你就必須作出決策。
我相信這樣思考編程是荒謬可笑的。核心不是東西之間的祖宗關係,而是它們可以為你做什麼。
當然,這就是介面進入 Go 的地方。但是它們已經是藍圖的一部分,那是真正的 Go 哲學。
如果說 C++ 和 Java 是關於類型繼承和類型分類的,Go 就是關於組合的。
Unix pipe 的最終發明人 Doug McIlroy 在 1964 (!) 這樣寫到:
我們應當像串連花園裡的龍頭和軟管一樣,用某種方式一段一段的將訊息資料連線起來。這同樣是 IO 使用的辦法。
這也是 Go 使用的辦法。Go 用了這個主意,並且將其向前推進了一大步。這是一個關於組合與串連的語言。
一個顯而易見的例子就是介面為我們提供的組合元件的方式。只要它實現了方法 M,就可以放在合適的地方,而不關心它到底是什麼東西。
另一個重要的例子是並發如何串連獨立啟動並執行計算。
並且也有一個不同尋常(卻非常簡單)的類型組合模式:嵌入。
這就是 Go 特有的組合技術,滋味與 C++ 或 Java 程式完全不同。
===========
有一個與此無關的 Go 設計我想要提一下:Go 被設計用於協助編寫大程式,由大團隊編寫和維護。
有一個觀點叫做“大編程”,不知怎麼回事 C++ 和 Java 主宰了這個領域。我相信這隻是一個曆史的失誤,或者是一個工業化的事故。但是一個廣泛被接受的信念是物件導向的設計可以做些事情。
我完全不相信那個。大軟體確實需要方法論保駕護航,但是用不著如此強的依賴管理和如此清晰的介面抽象,甚至如此華麗的文檔工具,沒有一樣是 C++ 做好的事情(儘管 Java 明顯做得更好一些)。
我們還不知道,因為沒有足夠的軟體採用 Go 來編寫,不過我有自信 Go 將在大編程領域脫穎而出。時間證明一切。
===========
現在,回到我演講一開始提到的那個令人驚奇的問題:
為什麼 Go,一個被設計為用於摧毀 C++ 的語言,並為並未獲得 C++ 程式員的芳心?
撇開玩笑不說,我認為那是因為 Go 和 C++ 有著完全不同的哲學。
C++ 是讓你的指尖解決所有的問題。我在 C++11 的 FAQ 上引用了這段內容:
C++ 與那些巨大增長的特別編寫的手工代碼相比,具有更加廣泛的抽象,優雅、靈活並且零成本的表達能力。
這個思考的方向與 Go 的不同。零成本不是目標,至少不是零 CPU 成本。Go 的主張更多考慮的是最小化程式員的工作量。
Go 不是無所不包的。你無法通過內建獲得所有東西。你無法精確控制每個細微的執行。例如沒有 RAII。而可以用垃圾收集作為代替。也沒有記憶體釋放函數。
你得到的是功能強大,但是容易理解的,容易用來構建一些用於串連組合解決問題的模組。這可能最終不像你使用其他語言編寫的解決方案那麼快,那麼精緻,在思想體繫上那麼明確,但它確實會更加容易編寫,容易閱讀,容易理解,容易維護,並且更加安全。
換句話說,當然,有些過於簡單:
Python 和 Ruby 程式員轉到 Go 是因為他們並未放棄太多的表達能力,但是獲得了效能,並且與並發共舞。
C++ 程式員無法轉到 Go 是因為他們經過艱辛的戰鬥才獲得對其語言的精確控制能力,而且也不想放棄任何已經獲得的東西。對於他們,軟體不僅僅是關於讓工作完成,而是關於用一個確定的方式完成。
那麼,問題是,Go 的成功能否反駁他們的世界觀。
我們應當在一開始的時候就意識到了一點。那些為 C++11 的新特性而興奮的人們是不會在意一個沒有這麼多特性的語言。即便最後發現這個語言能夠比他們所想象的提供更多。
謝謝大家。
我的看法
目標其實很清晰,想讓google內部的工程師都用這個,代替c/c++。不知道現在google內部對GO語言是什麼樣的狀況。 就我瞭解的而言,我覺得GO主要要解決的是大規模並發系統編程的問題:
1. 編譯速度:這種大系統還是很有影響的,GO通過不同的編譯器和依賴關係管理來提升編譯速度。
2. 關於抽象:C++/Java是通過類體系來描述抽象,GO沒有類主要是用介面和組合,這一點上和OO設計原則是一致的。
3. 語言層面支援並行:就像c可以實現多態,但遠遠比不上c++實現的多態那麼好維護,語言層面支援的東西還是很有優勢的。
4. 記憶體管理:我有點怕的是它支援記憶體管理,記憶體回收,可能是多慮了,其實自己管理記憶體在大規模程式上確實是個問題。無論如何,GO也表明這個問題是個難題,同時它也支援自己管理記憶體。暫時我就這麼認為了,我覺得GO是大規模並發系統這種需求催生出來的語言,所以應用也在這個領域,還很年輕有待成熟,記憶體記憶體回收也是比較難的一個課題,最大的優勢是並發。據說在google,youtube,豆瓣等有實際應用,它們可能都是這種情境。
元芳,你怎麼看?