[置頂] 爲什麼我們需要一門新語言——Go語言

編程語言已經非常多,偏性能敏感的編譯型語言有 C、C++、Java、C#、Delphi和Objective-C等,偏快速業務開發的動態解析型語言有 PHP、Python、Perl、Ruby、JavaScript和Lua等,面向特定領域的語言有 Erlang、R和MATLAB等,那麼我們爲什麼需要 Go這樣一門新語言呢? 在2000年前的單機時代, C語言是編程之王。隨着機器性能的提升、軟件規模與複雜度的提高,Java逐步取代了C的位置。儘管看起來 Java已經深獲人心,但 Java編程的體驗並未盡如人意。歷年來的編程語言排行榜(如圖 0-1所示)顯示, Java語言的市場份額在逐步下跌,並趨近於 C語言的水平,顯示了這門語言後勁不足。

enter image description here

圖0-1編程語言排行榜

Go語言官方自稱,之所以開發 Go語言,是因爲“近 10年來開發程序之難讓我們有點沮喪”。這一定位暗示了 Go語言希望取代 C和Java的地位,成爲最流行的通用開發語言。 Go希望成爲互聯網時代的 C語言。多數系統級語言(包括 Java和C#)的根本編程哲學來源於 C++,將C++的面向對象進一步發揚光大。但是Go語言的設計者卻有不同的看法,他們認爲C++ 真的沒啥好學的,值得學習的是 C語言。C語言經久不衰的根源是它足夠簡單。因此, Go語言也要足夠簡單!

那麼,互聯網時代的 C語言需要考慮哪些關鍵問題呢?首先,並行與分佈式支持。多核化和集羣化是互聯網時代的典型特徵。作爲一個互聯網時代的C語言,必須要讓這門語言操作多核計算機與計算機集羣如同操作單機一樣容易。其次,軟件工程支持。工程規模不斷擴大是產業發展的必然趨勢。單機時代語言可以只關心問題本身的解決,而互聯網時代的 C語言還需要考慮軟件品質保障和團隊協作相關的話題。最後,編程哲學的重塑。計算機軟件經歷了數十年的發展,形成了面向對象等多種學術流派。什麼纔是最佳的編程實踐?作爲互聯網時代的 C語言,需要回答這個問題。接下來我們來聊聊 Go語言在這些話題上是如何應對的。

併發與分佈式

多核化和集羣化是互聯網時代的典型特徵,那語言需要哪些特性來應對這些特徵呢?第一個話題是併發執行的“執行體”。執行體是個抽象的概念,在操作系統層面有多個概念與之對應,比如操作系統自己掌管的進程( process)、進程內的線程( thread)以及進程內的協程(coroutine,也叫輕量級線程)。多數語言在語法層面並不直接支持協程,而通過庫的方式支持的協程的功能也並不完整,比如僅僅提供協程的創建、銷燬與切換等能力。如果在這樣的協程中調用一個同步 IO操作,比如網絡通信、本地文件讀寫,都會阻塞其他的併發執行協程,從而無法真正達到協程本身期望達到的目標。

Go語言在語言級別支持協程,叫 goroutine。Go語言標準庫提供的所有系統調用( syscall)操作,當然也包括所有同步 IO操作,都會出讓 CPU給其他goroutine,這讓事情變得非常簡單。我們對比一下Java和Go,近距離觀摩下兩者對“執行體”的支持。

爲了簡化,我們在樣例中使用的是 Java標準庫中的線程,而不是協程,具體代碼如下:

public class MyThread implements Runnable { 
String arg; 
public MyThread(String a) { arg = a; } 
public void run() { // ... } 
public static void main(String[] args) { new Thread(new MyThread("test")).start(); // ... 
} 
} 

相同功能的代碼,在 Go語言中是這樣的:

func run(arg string) { 
// ... 
} 
func main() { 
go run("test") 
... 
} 

對比非常鮮明。我相信你已經明白爲什麼 Go語言會叫 Go語言了:Go語言獻給這個時代最好的禮物,就是加了 go這個關鍵字。當然也有人會說,叫 Go語言是因爲它是 Google出的。好吧,這也是個不錯的閒聊主題。 第二個話題是“執行體間的通信”。執行體間的通信包含幾個方式:

  • 執行體之間的互斥與同步
  • 執行體之間的消息傳遞

enter image description here

Go語言推薦採用“ Erlang風格的併發模型”的編程範式,儘管傳統的“共享內存模型”仍然被保留,允許適度地使用。在 Go語言中內置了消息隊列的支持,只不過它叫通道( channel)。兩個goroutine之間可以通過通道來進行交互。

軟件工程 單機時代的語言可以只關心問題本身的解決,但是隨着工程規模的不斷擴大,軟件複雜度的不斷增加,軟件工程也成爲語言設計層面要考慮的重要課題。多數軟件需要一個團隊共同去完成,在團隊協作的過程中,人們需要建立統一的交互語言來降低溝通的成本。規範化體現在多個層面,如:

  • 代碼風格規範
  • 錯誤處理規範
  • 包管理
  • 契約規範(接口)
  • 單元測試規範
  • 功能開發的流程規範

Go語言很可能是第一個將代碼風格強制統一的語言,例如 Go語言要求 public的變量必須以大寫字母開頭,private變量則以小寫字母開頭,這種做法不僅免除了public、private關鍵字,更重要的是統一了命名風格。 另外,Go語言對 { }應該怎麼寫進行了強制,比如以下風格是正確的:

if expression { ... } 

但下面這個寫法就是錯誤的:

if expression { ... } 

而C和Java語言中則對花括號的位置沒有任何要求。哪種更有利,這個見仁見智。但很顯然的是,所有的 Go代碼的花括號位置肯定是非常統一的。最有意思的其實還是 Go語言首創的錯誤處理規範:

f, err := os.Open(filename) 
if err != nil { log.Println("Open file failed:", err) return 
} defer f.Close() ... // 操作已經打開的 f文件

這裏有兩個關鍵點。其一是 defer關鍵字。 defer語句的含義是不管程序是否出現異常,均在函數退出時自動執行相關代碼。在上面的例子中,正是因爲有了 defer,才使得無論後續是否會出現異常,都可以確保文件被正確關閉。其二是 Go語言的函數允許返回多個值。大多數函數的最後一個返回值會爲 error類型,以在錯誤情況下返回詳細信息。 error類型只是一個系統內置的interface,如下:

type error interface { Error() string } 

有了error類型,程序出現錯誤的邏輯看起來就相當統一。在Java中,你可能這樣寫代碼來保證資源正確釋放:

Connection conn = ...; 
try { Statement stmt = ...; try { 
ResultSet rset = ...; try { 
... // 正常代碼 } finally { 
rset.close(); 
} } finally { 
stmt.close(); 
} } finally { 
conn.close(); } 

完成同樣的功能,相應的 Go代碼只需要寫成這樣:

conn := ... defer conn.Close() 
stmt := ... defer stmt.Close() 
rset := ... defer rset.Close() ... // 正常代碼

對比兩段代碼, Go語言處理錯誤的優勢顯而易見。當然,其實 Go語言帶給我們的驚喜還有很多,後續有機會我們可以就某個更具體的話題詳細展開來談一談。

編程哲學

計算機軟件經歷了數十年的發展,形成了多種學術流派,有面向過程編程、面向對象編程、函數式編程、面向消息編程等,這些思想究竟孰優孰劣,衆說紛紜。

C語言是純過程式的,這和它產生的歷史背景有關。 Java語言則是激進的面向對象主義推崇者,典型表現是它不能容忍體系裏存在孤立的函數。而 Go語言沒有去否認任何一方,而是用批判吸收的眼光,將所有編程思想做了一次梳理,融合衆家之長,但時刻警惕特性複雜化,極力維持語言特性的簡潔,力求小而精。

從編程範式的角度來說, Go語言是變革派,而不是改良派。對於C++、Java和C#等語言爲代表的面向對象( OO)思想體系,Go語言總體來說持保守態度,有限吸收。首先,Go語言反對函數和操作符重載(overload),而C++、Java和C#都允許出現同名函數或操作符,只要它們的參數列表不同。雖然重載解決了一小部分面向對象編程( OOP)的問題,但同樣給這些語言帶來了極大的負擔。而 Go語言有着完全不同的設計哲學,既然函數重載帶來了負擔,並且這個特性並不對解決任何問題有顯著的價值,那麼 Go就不提供它。其次,Go語言支持類、類成員方法、類的組合,但反對繼承,反對虛函數( virtual function)和虛函數重載。確切地說, Go也提供了繼承,只不過是採用了組合的文法來提供:

    type Foo struct { Base ... 
    } 

func (foo *Foo) Bar() { ... } 

再次,Go語言也放棄了構造函數( constructor)和析構函數(destructor)。由於Go語言中沒有虛函數,也就沒有 vptr,支持構造函數和析構函數就沒有太大的價值。本着“如果一個特性並不對解決任何問題有顯著的價值,那麼 Go就不提供它”的原則,構造函數和析構函數就這樣被Go語言的作者們幹掉了。

在放棄了大量的 OOP特性後,Go語言送上了一份非常棒的禮物:接口( interface)。你可能會說,除了 C這麼原始的語言外,還有什麼語言沒有接口呢?是的,多數語言都提供接口,但它們的接口都不同於 Go語言的接口。

Go語言中的接口與其他語言最大的一點區別是它的非侵入性。在 C++、Java和C#中,爲了實現一個接口,你需要從該接口繼承,具體代碼如下:

class Foo implements IFoo { // Java文法 ... } 
class Foo : public IFoo { // C++文法 ... } 
IFoo* foo = new Foo; 

在Go語言中,實現類的時候無需從接口派生,具體代碼如下:

type Foo struct { // Go 文法 ... } 
var foo IFoo = new(Foo) 

只要Foo實現了接口IFoo要求的所有方法,就實現了該接口,可以進行賦值。 Go語言的非侵入式接口,看似只是做了很小的文法調整,實則影響深遠。其一,Go語言的標準庫再也不需要繪製類庫的繼承樹圖。你只需要知道這個類實現了哪些方法,每個方法是啥含義就足夠了。其二,不用再糾結接口需要拆得多細才合理,比如我們實現了 File類,它有下面這些方法:

Read(buf []byte) (n int, err error) Write(buf []byte) (n int, err error) Seek(off int64, whence int) (pos int64, err error) Close() error 

那麼,到底是應該定義一個 IFile接口,還是應該定義一系列的 IReader、IWriter、 ISeeker和ICloser接口,然後讓File從它們派生好呢?事實上,脫離了實際的用戶場景,討論這兩個設計哪個更好並無意義。問題在於,實現 File類的時候,我怎麼知道外部會如何用它呢?

其三,不用爲了實現一個接口而專門導入一個包,而目的僅僅是引用其中的某個接口的定義。在Go語言中,只要兩個接口擁有相同的方法列表,那麼它們就是等同的,可以相互賦值,如對於以下兩個接口,第一個接口:

enter image description here

爲了引用另一個包中的接口而導入這個包的做法是不被推薦的。因爲多引用一個外部的包,就意味着更多的耦合。 除了OOP外,近年出現了一些小衆的編程哲學, Go語言對這些思想亦有所吸收。例如, Go語言接受了函數式編程的一些想法,支持匿名函數與閉包。再如, Go語言接受了以 Erlang語言爲代表的面向消息編程思想,支持 goroutine和通道,並推薦使用消息而不是共享內存來進行併發編程。總體來說, Go語言是一個非常現代化的語言,精小但非常強大。

小結 在十餘年的技術生涯中,我接觸過、使用過、喜愛過不同的編程語言,但總體而言, Go語言的出現是最讓我興奮的事情。我個人對未來 10年編程語言排行榜的趨勢判斷如下:

  • Java語言的份額繼續下滑,並最終被 C和Go語言超越;
  • C語言將長居編程榜第二的位置,並有望在 Go取代Java前重獲語言榜第一的寶座;
  • Go語言最終會取代 Java,居於編程榜之首。

由七牛雲存儲團隊編著的這本書將儘可能展現出 Go語言的迷人魅力。希望本書能夠讓更多人理解這門語言,熱愛這門語言,讓這門優秀的語言能夠落到實處,把程序員從以往繁雜的語言細節中解放出來,集中精力開發更加優秀的系統軟件。 



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章