摘要:文將詳細介紹 Golang 的語言特色以及它的優缺點和適用場景,帶着上述幾個疑問,爲讀者分析 Go 語言的各個方面,以幫助初入 IT 行業的程序員以及對 Go 感興趣的開發者進一步瞭解這個熱門語言。
本文分享自華爲雲社區《大紅大紫的 Golang 真的是後端開發中的萬能藥嗎?》,原文做者:Marvin Zhang 。前端
城外的人想進去,城裏的人想出來。-- 錢鍾書《圍城》java
隨着容器編排(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 年調查程序員技能的相關結果。git
那麼,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。github
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 語言的語法很是簡單,至少在變量聲明、結構體聲明、函數定義等方面顯得很是簡潔。segmentfault
變量的聲明不像 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 做爲實際工做開發語言的時候,須要全面瞭解其語言特性,從而作出最合理的技術選型。就像打網球同樣,不只須要掌握正反手,還要會發球、高壓球、截擊球等技術動做,這樣才能把網球打好。