摘要:文將詳細介紹 Golang 的語言特點以及它的優缺點和適用場景,帶著上述幾個疑問,為讀者分析 Go 語言的各個方面,以幫助初入 IT 行業的程式設計師以及對 Go 感興趣的開發者進一步瞭解這個熱門語言。
本文分享自華為雲社群《大紅大紫的 Golang 真的是後端開發中的萬能藥嗎?》,原文作者:Marvin Zhang 。
城外的人想進去,城裡的人想出來。-- 錢鍾書《圍城》
隨著容器編排(Container Orchestration)、微服務(Micro Services)、雲技術(Cloud Technology)等在 IT 行業不斷盛行,2009 年誕生於 Google 的 Golang(Go 語言,簡稱 Go)越來越受到軟體工程師的歡迎和追捧,成為如今炙手可熱的後端程式語言。在用 Golang 開發的軟體專案列表中,有 Docker(容器技術)、Kubernetes(容器編排)這樣的顛覆整個 IT 行業的明星級產品,也有像 Prometheus(監控系統)、Etcd(分散式儲存)、InfluxDB(時序資料庫)這樣的強大實用的知名專案。當然,Go 語言的應用領域也絕不侷限於容器和分散式系統。如今很多大型網際網路企業在大量使用 Golang 構建後端 Web 應用,例如今日頭條、京東、七牛雲等;長期被 Python 統治的框架爬蟲領域也因為簡單而易用的爬蟲框架 Colly 的崛起而不斷受到 Golang 的挑戰。Golang 已經成為了如今大多數軟體工程師最想學習的程式語言。下圖是 HackerRank 在 2020 年調查程式設計師技能的相關結果。
那麼,Go 語言真的是後端開發人員的救命良藥呢?它是否能夠有效提高程式設計師們的技術實力和開發效率,從而幫助他們在職場上更進一步呢?Go 語言真的值得我們花大量時間深入學習麼?本文將詳細介紹 Golang 的語言特點以及它的優缺點和適用場景,帶著上述幾個疑問,為讀者分析 Go 語言的各個方面,以幫助初入 IT 行業的程式設計師以及對 Go 感興趣的開發者進一步瞭解這個熱門語言。
Golang 誕生於網際網路巨頭 Google,而這並不是一個巧合。我們都知道,Google 有一個 20% 做業餘專案(Side Project)的企業文化,允許工程師們能夠在輕鬆的環境下創造一些具有顛覆性創新的產品。而 Golang 也正是在這 20% 時間中不斷孵化出來。Go 語言的創始者也是 IT 界內大名鼎鼎的行業領袖,包括 Unix 核心團隊成員 Rob Pike、C 語言作者 Ken Thompson、V8 引擎核心貢獻者 Robert Griesemer。Go 語言被大眾所熟知還是源於容器技術 Docker 在 2014 年被開源後的爆發式發展。之後,Go 語言因為其簡單的語法以及迅猛的編譯速度受到大量開發者的追捧,也誕生了很多優秀的專案,例如 Kubernetes。
Go 語言相對於其他傳統熱門程式語言來說,有很多優點,特別是其高效編譯速度和天然並行特性,讓其成為快速開發分散式應用的首選語言。Go 語言是靜態型別語言,也就是說 Go 語言跟 Java、C# 一樣需要編譯,而且有完備的型別系統,可以有效減少因型別不一致導致的程式碼品質問題。因此,Go 語言非常適合構建對穩定性和靈活性均有要求的大型 IT 系統,這也是很多大型網際網路公司用 Golang 重構老程式碼的重要原因:傳統的靜態 OOP 語言(例如 Java、C#)穩定性高但缺乏靈活性;而動態語言(例如 PHP、Python、Ruby、Node.js)靈活性強但缺乏穩定性。因此,「熊掌和魚兼得」 的 Golang,受到開發者們的追捧是自然而然的事情,畢竟,「天下苦 Java/PHP/Python/Ruby 們久矣「。
不過,Go 語言並不是沒有缺點。用辯證法的思維方式可以推測,Golang 的一些突出特性將成為它的雙刃劍。例如,Golang 語法簡單的優勢特點將限制它處理複雜問題的能力。尤其是 Go 語言缺乏泛型(Generics)的問題,導致它構建通用框架的複雜度大增。雖然這個突出問題在 2.0 版本很可能會有效解決,但這也反映出來明星程式語言也會有缺點。當然,Go 的缺點還不止於此,Go 語言使用者還會吐槽其囉嗦的錯誤處理方式(Error Handling)、缺少嚴格約束的鴨子型別(Duck Typing)、日期格式問題等。下面,我們將從 Golang 語言特點開始,由淺入深多維度深入分析 Golang 的優缺點以及專案適用場景。
Go 語言的語法非常簡單,至少在變數宣告、結構體宣告、函數定義等方面顯得非常簡潔。
變數的宣告不像 Java 或 C 那樣囉嗦,在 Golang 中可以用 := 這個語法來宣告新變數。例如下面這個例子,當你直接使用 := 來定義變數時,Go 會自動將賦值物件的型別宣告為賦值來源的型別,這節省了大量的程式碼。
func main() {
valInt := 1 // 自動推斷 int 型別
valStr := "hello" // 自動推斷為 string 型別
valBool := false // 自動推斷為 bool 型別
}
Golang 還有很多幫你節省程式碼的地方。你可以發現 Go 中不會強制要求用 new 這個關鍵詞來生成某個類(Class)的新範例(Instance)。而且,對於公共和私有屬性(變數和方法)的約定不再使用傳統的 public 和 private 關鍵詞,而是直接用屬性變數首字母的大小寫來區分。下面一些例子可以幫助讀者理解這些特點。
// 定義一個 struct 類
type SomeClass struct {
PublicVariable string // 公共變數
privateVariable string // 私有變數
}
// 公共方法
func (c *SomeClass) PublicMethod() (result string) {
return "This can be called by external modules"
}
// 私有方法
func (c *SomeClass) privateMethod() (result string) {
return "This can only be called in SomeClass"
}
func main() {
// 生成範例
someInstance := SomeClass{
PublicVariable: "hello",
privateVariable: "world",
}
}
如果你用 Java 來實現上述這個例子,可能會看到冗長的 .java 類檔案,例如這樣。
// SomeClass.java
public SomeClass {
public String PublicVariable; // 公共變數
private String privateVariable; // 私有變數
// 建構函式
public SomeClass(String val1, String val2) {
this.PublicVariable = val1;
this.privateVariable = val2;
}
// 公共方法
public String PublicMethod() {
return "This can be called by external modules";
}
// 私有方法
public String privateMethod() {
return "This can only be called in SomeClass";
}
}
...
// Application.java
public Application {
public static void main(String[] args) {
// 生成範例
SomeClass someInstance = new SomeClass("hello", "world");
}
}
可以看到,在 Java 程式碼中除了容易看花眼的多層花括號以外,還充斥著大量的 public、private、static、this 等修飾用的關鍵詞,顯得異常囉嗦;而 Golang 程式碼中則靠簡單的約定,例如首字母大小寫,避免了很多重複性的修飾詞。當然,Java 和 Go 在型別系統上還是有一些區別的,這也導致 Go 在處理複雜問題顯得有些力不從心,這是後話,後面再討論。總之,結論就是 Go 的語法在靜態型別程式語言中非常簡潔。
Go 語言之所以成為分散式應用的首選,除了它效能強大以外,其最主要的原因就是它天然的並行程式設計。這個並行程式設計特性主要來自於 Golang 中的協程(Goroutine)和通道(Channel)。下面是使用協程的一個例子。
func asyncTask() {
fmt.Printf("This is an asynchronized task")
}
func syncTask() {
fmt.Printf("This is a synchronized task")
}
func main() {
go asyncTask() // 非同步執行,不阻塞
syncTask() // 同步執行,阻塞
go asyncTask() // 等待前面 syncTask 完成之後,再非同步執行,不阻塞
}
可以看到,關鍵詞 go 加函數呼叫可以讓其作為一個非同步函數執行,不會阻塞後面的程式碼。而如果不加 go 關鍵詞,則會被當成是同步程式碼執行。如果讀者熟悉 JavaScript 中的 async/await、Promise 語法,甚至是 Java、Python 中的多執行緒非同步程式設計,你會發現它們跟 Go 非同步程式設計的簡單程度不是一個量級的!
非同步函數,也就是協程之間的通訊可以用 Go 語言特有的通道來實現。下面是關於通道的一個例子。
func longTask(signal chan int) {
// 不帶引數的 for
// 相當於 while 迴圈
for {
// 接收 signal 通道傳值
v := <- signal
// 如果接收值為 1,停止迴圈
if v == 1 {
break
}
time.Sleep(1 * Second)
}
}
func main() {
// 宣告通道
sig := make(chan int)
// 非同步呼叫 longTask
go longTask(sig)
// 等待 1 秒鐘
time.Sleep(1 * time.Second)
// 向通道 sig 傳值
sig <- 1
// 然後 longTask 會接收 sig 傳值,終止迴圈
}
Go 語言不是嚴格的物件導向程式設計(OOP),它採用的是面向介面程式設計(IOP),是相對於 OOP 更先進的程式設計模式。作為 OOP 體系的一部分,IOP 更加強調規則和約束,以及介面型別方法的約定,從而讓開發人員儘可能的關注更抽象的程式邏輯,而不是在更細節的實現方式上浪費時間。很多大型專案採用的都是 IOP 的程式設計模式。如果想了解更多面向介面程式設計,請檢視 「碼之道」 個人技術部落格的往期文章《為什麼說 TypeScript 是開發大型前端專案的必備語言》,其中有關於面向介面程式設計的詳細講解。
Go 語言跟 TypeScript 一樣,也是採用鴨子型別的方式來校驗介面繼承。下面這個例子可以描述 Go 語言的鴨子型別特性。
// 定義 Animal 介面
interface Animal {
Eat() // 宣告 Eat 方法
Move() // 宣告 Move 方法
}
// ==== 定義 Dog Start ====
// 定義 Dog 類
type Dog struct {
}
// 實現 Eat 方法
func (d *Dog) Eat() {
fmt.Printf("Eating bones")
}
// 實現 Move 方法
func (d *Dog) Move() {
fmt.Printf("Moving with four legs")
}
// ==== 定義 Dog End ====
// ==== 定義 Human Start ====
// 定義 Human 類
type Human struct {
}
// 實現 Eat 方法
func (h *Human) Eat() {
fmt.Printf("Eating rice")
}
// 實現 Move 方法
func (h *Human) Move() {
fmt.Printf("Moving with two legs")
}
// ==== 定義 Human End ====
可以看到,雖然 Go 語言可以定義介面,但跟 Java 不同的是,Go 語言中沒有顯示宣告介面實現(Implementation)的關鍵詞修飾語法。在 Go 語言中,如果要繼承一個介面,你只需要在結構體中實現該介面宣告的所有方法。這樣,對於 Go 編譯器來說你定義的類就相當於繼承了該介面。在這個例子中,我們規定,只要既能吃(Eat)又能活動(Move)的東西就是動物(Animal)。而狗(Dog)和人(Human)恰巧都可以吃和動,因此它們都被算作動物。這種依靠實現方法匹配度的繼承方式,就是鴨子型別:如果一個動物看起來像鴨子,叫起來也像鴨子,那它一定是鴨子。這種鴨子型別相對於傳統 OOP 程式語言顯得更靈活。但是,後面我們會討論到,這種程式設計方式會帶來一些麻煩。
Go 語言的錯誤處理是臭名昭著的囉嗦。這裡先給一個簡單例子。
package main
import "fmt"
func isValid(text string) (valid bool, err error){
if text == "" {
return false, error("text cannot be empty")
}
return text == "valid text", nil
}
func validateForm(form map[string]string) (res bool, err error) {
for _, text := range form {
valid, err := isValid(text)
if err != nil {
return false, err
}
if !valid {
return false, nil
}
}
return true, nil
}
func submitForm(form map[string]string) (err error) {
if res, err := validateForm(form); err != nil || !res {
return error("submit error")
}
fmt.Printf("submitted")
return nil
}
func main() {
form := map[string]string{
"field1": "",
"field2": "invalid text",
"field2": "valid text",
}
if err := submitForm(form); err != nil {
panic(err)
}
}
雖然上面整個程式碼是虛構的,但可以從中看出,Go 程式碼中充斥著 if err := ...; err != nil { ... } 之類的錯誤判斷語句。這是因為 Go 語言要求開發者自己管理錯誤,也就是在函數中的錯誤需要顯式丟擲來,否則 Go 程式不會做任何錯誤處理。因為 Go 沒有傳統程式語言的 try/catch 針對錯誤處理的語法,所以在錯誤管理上缺少靈活度,導致了 「err 滿天飛」 的局面。
不過,辯證法則告訴我們,這種做法也是有好處的。第一,它強制要求 Go 語言開發者從程式碼層面來規範錯誤的管理方式,這驅使開發者寫出更健壯的程式碼;第二,這種顯式返回錯誤的方式避免了 「try/catch 一把梭」,因為這種 「一時爽」 的做法很可能導致 Bug 無法準確定位,從而產生很多不可預測的問題;第三,由於沒有 try/catch 的括號或額外的程式碼塊,Go 程式程式碼整體看起來更清爽,可讀性較強。
Go 語言肯定還有很多其他特性,但筆者認為以上的特性是 Go 語言中比較有特色的,是區分度比較強的特性。Go 語言其他一些特性還包括但不限於如下內容。
import "github.com/crawlab-team/go-trace"
)前面介紹了 Go 的很多語言特性,想必讀者已經對 Golang 有了一些基本的瞭解。其中的一些語言特性也暗示了它相對於其他程式語言的優缺點。Go 語言雖然現在很火,在稱讚並擁抱 Golang 的同時,不得不瞭解它的一些缺點。
這裡筆者不打算長篇大論的解析 Go 語言的優劣,而是將其中相關的一些事實列舉出來,讀者可以自行判斷。以下是筆者總結的 Golang 語言特性的不完整優缺點對比列表。
其實,每一個特性在某種情境下都有其相應的優勢和劣勢,不能一概而論。就像 Go 語言採用的靜態型別和麵向介面程式設計,既不缺少型別約束,也不像嚴格 OOP 那樣冗長繁雜,是介於動態語言和傳統靜態型別 OOP 語言之間的現代程式語言。這個定位在提升 Golang 開發效率的同時,也閹割了不少必要 OOP 語法特性,從而缺乏快速構建通用工程框架的能力(這裡不是說 Go 無法構建通用框架,而是它沒有 Java、C# 這麼容易)。另外,Go 語言 「奇葩」 的錯誤處理規範,讓 Go 開發者們又愛又恨:可以開發出更健壯的應用,但同時也犧牲了一部分程式碼的簡潔性。要知道,Go 語言的設計理念是為了 「大道至簡」,因此才會在追求高效能的同時設計得儘可能簡單。
無可否認的是,Go 語言內建的並行支援是非常近年來非常創新的特性,這也是它被分散式系統廣泛採用的重要原因。同時,它相對於動輒編譯十幾分鐘的 Java 來說是非常快的。此外,Go 語言沒有因為語法簡單而犧牲了穩定性;相反,它從簡單的約束規範了整個 Go 專案程式碼風格。因此,「快」(Fast)、「簡」(Concise)、「穩」(Robust)是 Go 語言的設計目的。我們在對學習 Golang 的過程中不能無腦的接納它的一切,而是應該根據它自身的特性判斷在實際專案應用中的情況。
經過前文關於 Golang 各個維度的討論,我們可以得出結論:Go 語言並不是後端開發的萬能藥。在實際開發工作中,開發者應該避免在任何情況下無腦使用 Golang 作為後端開發語言。相反,工程師在決定技術選型之前應該全面瞭解候選技術(語言、框架或架構)的方方面面,包括候選技術與業務需求的切合度,與開發團隊的融合度,以及其學習、開發、時間成本等因素。筆者在學習了包括前後端的一些程式語言之後,發現它們各自有各自的優勢,也有相應的劣勢。如果一門程式語言能廣為人知,那它絕對不會是一門糟糕語言。因此,筆者不會斷言 「XXX 是世界上最好的語言「,而是給讀者分享個人關於特定應用場景下技術選型的思路。當然,本文是針對 Go 語言的技術文,接下來筆者將分享一下個人認為 Golang 最適合的應用場景。
Golang 是非常適合在分散式應用場景下開發的。分散式應用的主要目的是儘可能多的利用計算資源和網路頻寬,以求最大化系統的整體效能和效率,其中重要的需求功能就是並行(Concurrency)。而 Go 是支援高並行和非同步程式設計方面的佼佼者。
前面已經提到,Go 語言內建了協程(Goroutine)和通道(Channel)兩大並行特性,這使後端開發者進行非同步程式設計變得非常容易。Golang 中還內建了sync 庫,包含 Mutex(互斥鎖)、WaitGroup(等待組)、Pool(臨時物件池)等介面,幫助開發者在並行程式設計中能更安全的掌控 Go 程式的並行行為。Golang 還有很多分散式應用開發工具,例如分散式儲存系統(Etcd、SeaweedFS)、RPC 庫(gRPC、Thrift)、主流資料庫 SDK(mongo-driver、gnorm、redigo)等。這些都可以幫助開發者有效的構建分散式應用。
稍微瞭解網路爬蟲的開發者應該會聽說過 Scrapy,再不濟也是 Python。市面上關於 Python 網路爬蟲的技術書籍數不勝數,例如崔慶才的《Python 3 網路開發實戰》和韋世東的《Python 3 網路爬蟲寶典》。用 Python 編寫的高效能爬蟲框架 Scrapy,自發布以來一直是爬蟲工程師的首選。
不過,由於近期 Go 語言的迅速發展,越來越多的爬蟲工程師注意到用 Golang 開發網路爬蟲的巨大優勢。其中,用 Go 語言編寫的 Colly 爬蟲框架,如今在 Github 上已經有 13k+ 標星。其簡潔的 API 以及高效的採集速度,吸引了很多爬蟲工程師,佔據了爬蟲界一哥 Scrapy 的部分份額。前面已經提到,Go 語言內建的並行特性讓嚴重依賴網路頻寬的爬蟲程式更加高效,很大的提高了資料採集效率。另外,Go 語言作為靜態語言,相對於動態語言 Python 來說有更好的約束下,因此健壯性和穩定性都更好。
Golang 有很多優秀的後端框架,它們大部分都非常完備的支援了現代後端系統的各種功能需求:RESTful API、路由、中介軟體、設定、鑑權等模組。而且用 Golang 寫的後端應用效能很高,通常有非常快的響應速度。筆者曾經在開源爬蟲管理平臺 Crawlab 中用 Golang 重構了 Python 的後端 API,響應速度從之前的幾百毫秒優化到了幾十毫秒甚至是幾毫秒,用實踐證明 Go 語言在後端效能方面全面碾壓動態語言。Go 語言中比較知名的後端框架有 Gin、Beego、Echo、Iris。
當然,這裡並不是說用 Golang 寫後端就完全是一個正確的選擇。筆者在工作中會用到 Java 和 C#,用了各自的主流框架(SpringBoot 和 .Net Core)之後,發現這兩門傳統 OOP 語言雖然語法囉嗦,但它們的語法特性很豐富,特別是泛型,能夠輕鬆應對一些邏輯複雜、重複性高的業務需求。因此,筆者認為在考慮用 Go 來編寫後端 API 時候,可以提前調研一下 Java 或 C#,它們在寫後端業務功能方面做得非常棒。
本篇文章從 Go 語言的主要語法特性入手,循序漸進分析了 Go 語言作為後端程式語言的優點和缺點,以及其在實際軟體專案開發中的試用場景。筆者認為 Go 語言與其他語言的主要區別在於語法簡潔、天然支援並行、面向介面程式設計、錯誤處理等方面,並且對各個語言特性在正反兩方面進行了分析。最後,筆者根據之前的分析內容,得出了 Go 語言作為後端開發程式語言的適用場景,也就是分散式應用、網路爬蟲以及後端API。
當然,Go 語言的實際應用領域還不限於此。實際上,不少知名資料庫都是用 Golang 開發的,例如時序資料庫 Prometheus 和 InfluxDB、以及有 NewSQL 之稱的 TiDB。此外,在機器學習方面,Go 語言也有一定的優勢,只是目前來說,Google 因為 Swift 跟 TensorFlow 的意向合作,似乎還沒有大力推廣 Go 在機器學習方面的應用,不過一些潛在的開源專案已經湧現出來,例如 GoLearn、GoML、Gorgonia 等。
在理解 Go 語言的優勢和適用場景的同時,我們必須意識到 Go 語言並不是全能的。它相較於其他一些主流框架來說也有一些缺點。開發者在準備採用 Go 作為實際工作開發語言的時候,需要全面瞭解其語言特性,從而做出最合理的技術選型。就像打網球一樣,不僅需要掌握正反手,還要會發球、高壓球、截擊球等技術動作,這樣才能把網球打好。