為什麼我們需要一門新語言——Go語言

turingbooks發表於2012-08-14

程式語言已經非常多,偏效能敏感的編譯型語言有 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語言的迷人魅力。希望本書能夠讓更多人理解這門語言,熱愛這門語言,讓這門優秀的語言能夠落到實處,把程式設計師從以往繁雜的語言細節中解放出來,集中精力開發更加優秀的系統軟體。 摘自:《Go語言程式設計》

enter image description here

相關文章