優勢
第一個理由:對初學者足夠友善,能夠快速上手。
業界都公認:Go 是一種非常簡單的語言。Go 的設計者們在發布 Go 1.0 版本和兼容性規范后,似乎就把主要精力放在精心打磨 Go 的實現、改進語言周邊工具鏈,還有提升 Go 開發者體驗上了。演化了十多年,Go 新增的語言特性也同樣是“屈指可數”。
第二個理由:生產力與性能的最佳結合。
Go 創建的最初目的,就是構建流行的、高性能的服務器端編程語言,用以替代當時在這個領域使用較多的 Java 和 C++。而且,Go 也實現了它的這個目標。
Go 語言的性能在帶有 GC 和運行時的語言中名列前茅,與不帶 GC 的靜態編程語言(比如 C/C++)之間也沒有數量級的差距。在各大基準測試網站上,在相同的資源消耗水平的前提下,Go 的性能雖然低于 C++,但高出 Java 不少。
如果你熟悉的是動態語言,那也完全不用擔心。Go 的大部分早期采用者,就來自動態語言程序員群體,包括 Python、JavaScript、Ruby 和 PHP 等語言的使用群體。因為和動態語言相比,Go 能夠在保持生產力的同時,大幅度提高性能。雖然 Go 代碼行數要多于 Python,但他們收獲了近 10 倍的性能提升。
現在,Go 已經成為了云基礎架構語言,它在云原生基礎設施、中間件與云服務領域大放異彩。同時,GO 在 DevOps/SRE、區塊鏈、命令行交互程序(CLI)、Web 服務,還有數據處理等方面也有大量擁躉,我們甚至可以看到 Go 在微控制器、機器人、游戲領域也有廣泛應用。
第三個理由:快樂又有“錢景”。
Go 最初的目標,就是重新為開發人員帶來快樂。這個快樂來自哪里呢?相比 C/C++,甚至是 Java 來說,Go 在開發體驗上的確有很大提升。籠統來說,這個提升來自于簡單的語法、得心應手的工具鏈、豐富和健壯的標準庫,還有生產力與性能的完美結合、免除內存管理的心智負擔,對并發設計的原生支持等等。而這些良好的體驗,恰恰是你寫 Go 代碼時的快樂源泉。
有報告表明,在騰訊、字節跳動、Uber 等許多公司,Go 都是極其受歡迎,在字節跳動、Uber 內部甚至已經成長為主力語言。
誕生
Go 語言的創始人有三位,分別是圖靈獎獲得者、C 語法聯合發明人、Unix 之父肯·湯普森(Ken Thompson),Plan 9 操作系統領導者、UTF-8 編碼的最初設計者羅伯·派克(Rob Pike),以及 Java 的 HotSpot 虛擬機和 Chrome 瀏覽器的 JavaScript V8 引擎的設計者之一羅伯特·格瑞史莫(Robert Griesemer)。
之所以有這種想法,是因為當時的谷歌內部主要使用 C++ 語言構建各種系統,但 C++ 的巨大復雜性、編譯構建速度慢以及在編寫服務端程序時對并發支持的不足,讓三位大佬覺得十分不便,他們就想著設計一門新的語言。在他們的初步構想中,這門新語言應該是能夠給程序員帶來快樂、匹配未來硬件發展趨勢并適合用來開發谷歌內部大規模網絡服務程序的。
特性
簡單
在 Go 語言中看不到傳統的面向對象的類、構造函數與繼承,看不到結構化的異常處理,也看不到本屬于函數編程范式的語法元素。
Go 語言自身實現起來并不容易,但這些復雜性被 Go 語言的設計者們“隱藏”了,所以 Go 語法層面上呈現了這樣的狀態:
- 僅有 25 個關鍵字,主流編程語言最少;
- 內置垃圾收集器,降低開發人員內存管理的心智負擔;
- 首字母大小寫決定可見性,無需通過額外關鍵字修飾;
- 變量初始為類型零值,避免以隨機值作為初值的問題;
- 內置數組邊界檢查,極大減少越界訪問帶來的安全隱患;
- 內置并發支持,簡化并發程序設計;
- 內置接口類型,為組合的設計哲學奠定基礎;
- 原生提供完善的工具鏈,開箱即用;
- … …
顯式
在 C 語言中,下面這段代碼可以正常編譯并輸出正確結果:
//C 語言在編譯c = a + b這一行時,會自動將短整型變量 a 和整型變量 b,先轉換為 long 類型然后相加,并將所得結果存儲在 long 類型變量 c 中。
#include <stdio.h>
int main() {short int a = 5;int b = 8;long c = 0;c = a + b;printf("%ld\n", c);
}
那如果換成 Go 來實現這個計算會怎么樣呢?
package main
import "fmt"
func main() {var a int16 = 5var b int = 8var c int64c = a + bfmt.Printf("%d\n", c)
}
如果我們編譯這段程序,將得到類似這樣的編譯器錯誤:“invalid operation: a + b (mismatched types int16 and int)
”。我們能看到 Go 與 C 語言的隱式自動類型轉換不同,Go 不允許不同類型的整型變量進行混合計算
因此,如果要使這段代碼通過編譯,我們就需要對變量 a 和 b 進行顯式轉型,就像下面代碼段中這樣:
c = int64(a) + int64(b)
fmt.Printf("%d\n", c)
除此之外,Go 語言采用了顯式的基于值比較的錯誤處理方案,函數 / 方法中的錯誤都會通過 return 語句顯式地返回,并且通常調用者不能忽略對返回的錯誤的處理。
這種有悖于“主流語言潮流”的錯誤處理機制還一度讓開發者詬病,社區也提出了多個新錯誤處理方案,但或多或少都包含隱式的成分,都被 Go 開發團隊一一否決了,這也與顯式的設計哲學不無關系。
組合
Go 推崇的是組合的設計哲學。
在 Go 語言設計層面,Go 設計者為開發者們提供了正交的語法元素,以供后續組合使用,包括:
- Go 語言無類型層次體系,各類型之間是相互獨立的,沒有子類型的概念;
- 每個類型都可以有自己的方法集合,類型定義與方法實現是正交獨立的;
- 實現某個接口時,無需像 Java 那樣采用特定關鍵字修飾;
- 包之間是相對獨立的,沒有子包的概念。
Go 語言其實是為我們呈現了這樣的一幅圖景:一座座沒有關聯的“孤島”,但每個島內又都很精彩。那么現在擺在面前的工作,就是在這些孤島之間以最適當的方式建立關聯,并形成一個整體。而 Go 選擇采用的組合方式,也是最主要的方式。
Go 語言為支撐組合的設計提供了類型嵌入(Type Embedding)。通過類型嵌入,我們可以將已經實現的功能嵌入到新類型中,以快速滿足新類型的功能需求,這種方式有些類似經典面向對象語言中的“繼承”機制。但區別其實很大
被嵌入的類型和新類型兩者之間沒有任何關系,甚至相互完全不知道對方的存在,更沒有那種父類、子類的關系,以及向上、向下轉型。
垂直組合
通過新類型實例調用方法時,方法的匹配主要取決于方法名字,而不是類型。這種組合方式,我稱之為垂直組合,即通過類型嵌入,快速讓一個新類型“復用”其他類型已經實現的能力,實現功能的垂直擴展。
// $GOROOT/src/sync/pool.go
type poolLocal struct {private interface{} shared []interface{}Mutex pad [128]byte
}
//我們在 poolLocal 這個結構體類型中嵌入了類型 Mutex,這就使得 poolLocal 這個類型具有了互斥同步的能力
// 我們可以通過 poolLocal 類型的變量,直接調用 Mutex 類型的方法 Lock 或 Unlock。
另外,我們在標準庫中還會經常看到類似如下定義接口類型的代碼段:
// $GOROOT/src/io/io.go
type ReadWriter interface {ReaderWriter
}
//這里標準庫通過 嵌入接口類型的方式來實現接口行為的聚合,組成大接口,這種方式在標準庫中尤為常用,并且已經成為了 Go 語言的一種慣用法。
水平組合
垂直組合本質上是一種“能力繼承”,采用嵌入方式定義的新類型繼承了嵌入類型的能力。
Go 還有一種常見的組合方式,叫水平組合。和垂直組合的能力繼承不同,水平組合是一種能力委托(Delegate),我們通常使用接口類型來實現水平組合。
Go 語言中的接口是一個創新設計,它只是方法集合,并且它與實現者之間的關系無需通過顯式關鍵字修飾
水平組合的模式有很多,比如一種常見方法就是,通過接受接口類型參數的普通函數進行組合,如以下代碼段所示:
// $GOROOT/src/io/ioutil/ioutil.go
func ReadAll(r io.Reader)([]byte, error)
// $GOROOT/src/io/io.go
func Copy(dst Writer, src Reader)(written int64, err error)
也就是說,函數 ReadAll 通過 io.Reader 這個接口,將 io.Reader 的實現與 ReadAll 所在的包低耦合地水平組合在一起了,從而達到從任意實現 io.Reader 的數據源讀取所有數據的目的。
并發
CPU 都是靠提高主頻來改進性能的,但是主頻提高導致 CPU 的功耗和發熱量劇增,反過來制約了 CPU 性能的進一步提高。2007 年開始,處理器廠商的競爭焦點從主頻轉向了多核。
在這種大背景下,Go 的設計者在決定去創建一門新語言的時候,果斷將面向多核、原生支持并發作為了新語言的設計原則之一。并且,Go 放棄了傳統的基于操作系統線程的并發模型,而采用了用戶層輕量級線程,Go 將之稱為 goroutine。
goroutine
占用的資源非常小,Go 運行時默認為每個 goroutine 分配的棧空間僅 2KB。goroutine 調度的切換也不用陷入(trap)操作系統內核層完成,代價很低。因此,一個 Go 程序中可以創建成千上萬個并發的 goroutine。而且,所有的 Go 代碼都在 goroutine 中執行,哪怕是 go 運行時的代碼也不例外。- 同時,Go 還內置了并發設計的原語:
channel
和select
。開發者可以通過語言內置的 channel 傳遞消息或實現同步,并通過select
實現多路channel
的并發控制。
采用并發方案設計的程序在單核處理器上也是可以正常運行的,也許在單核上的處理性能可能不如非并發方案。但隨著處理器核數的增多,并發方案可以自然地提高處理性能。
而且,并發與組合的哲學是一脈相承的,并發是一個更大的組合的概念,它在程序設計的全局層面對程序進行拆解組合,再映射到程序執行層面上:goroutines 各自執行特定的工作,通過 channel+select 將 goroutines 組合連接起來。
面向工程
Go 語言設計的初衷,就是面向解決真實世界中 Google 內部大規模軟件開發存在的各種問題,為這些問題提供答案,這些問題包括:程序構建慢、依賴管理失控、代碼難于理解、跨語言構建難等。
語法是編程語言的用戶接口,它直接影響開發人員對于這門語言的使用體驗。在面向工程設計哲學的驅使下,Go 在語法設計細節上做了精心的打磨。比如:
- 重新設計編譯單元和目標文件格式,實現 Go 源碼快速構建,讓大工程的構建時間縮短到類似動態語言的交互式解釋的編譯速度;
- 如果源文件導入它不使用的包,則程序將無法編譯。這可以充分保證任何 Go 程序的依賴樹是精確的。這也可以保證在構建程序時不會編譯額外的代碼,從而最大限度地縮短編譯時間;
- 去除包的循環依賴,循環依賴會在大規模的代碼中引發問題,因為它們要求編譯器同時處理更大的源文件集,這會減慢增量構建;
- 包路徑是唯一的,而包名不必唯一的。導入路徑必須唯一標識要導入的包,而名稱只是包的使用者如何引用其內容的約定。
- 故意不支持默認函數參數。因為在規模工程中,很多開發者利用默認函數參數機制,向函數添加過多的參數以彌補函數 API 的設計缺陷,這會導致函數擁有太多的參數,降低清晰度和可讀性;
- 增加類型別名(type alias),支持大規模代碼庫的重構。
由于誕生年代較晚,而且目標比較明確,Go 在標準庫中提供了各類高質量且性能優良的功能包,其中的net/http、crypto、encoding
等包充分迎合了云原生時代的關于 API/RPC Web
服務的構建需求。
而且,開發人員在工程過程中肯定是需要使用工具的,Go 語言就提供了足以讓所有其它主流語言開發人員羨慕的工具鏈,工具鏈涵蓋了編譯構建、代碼格式化、包依賴管理、靜態代碼檢查、測試、文檔生成與查看、性能剖析、語言服務器、運行時程序跟蹤等方方面面。
gofmt
這里值得重點介紹的是 gofmt
,它統一了 Go 語言的代碼風格,Go 開發者可以更加專注于領域業務中。
在提供豐富的工具鏈的同時,Go 在標準庫中提供了官方的詞法分析器、語法解析器和類型檢查器相關包,開發者可以基于這些包快速構建并擴展 Go 工具鏈。