摘要:文將詳細介紹 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 簡介
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 語言其他一些特性還包括但不限於如下內容。
- 編譯迅速
- 跨平臺
- defer 延遲執行
- select/case 通道選擇
- 直接編譯成可執行程式
- 非常規依賴管理(可以直接引用 Github 倉庫作為依賴,例如
import "github.com/crawlab-team/go-trace"
) - 非常規日期格式(格式為 “2006-01-02 15:04:05”,你沒看錯,據說這就是 Golang 的創始時間!)
優缺點概述
前面介紹了 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 來說有更好的約束下,因此健壯性和穩定性都更好。
後端 API
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 作為實際工作開發語言的時候,需要全面瞭解其語言特性,從而做出最合理的技術選型。就像打網球一樣,不僅需要掌握正反手,還要會發球、高壓球、截擊球等技術動作,這樣才能把網球打好。