大紅大紫的 Golang 真的是後端開發中的萬能藥嗎?

前言

城外的人想進去,城裏的人想出來。-- 錢鍾書《圍城》

隨着容器編排(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 年調查程序員技能的相關結果。html

hackerrank-survey-2020

那麼,Go 語言真的是後端開發人員的救命良藥呢?它是否可以有效提升程序員們的技術實力和開發效率,從而幫助他們在職場上更進一步呢?Go 語言真的值得咱們花大量時間深刻學習麼?本文將詳細介紹 Golang 的語言特色以及它的優缺點和適用場景,帶着上述幾個疑問,爲讀者分析 Go 語言的各個方面,以幫助初入 IT 行業的程序員以及對 Go 感興趣的開發者進一步瞭解這個熱門語言。前端

Golang 簡介

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。java

Go 語言相對於其餘傳統熱門編程語言來講,有不少優勢,特別是其高效編譯速度自然併發特性,讓其成爲快速開發分佈式應用的首選語言。Go 語言是靜態類型語言,也就是說 Go 語言跟 Java、C# 同樣須要編譯,並且有完備的類型系統,能夠有效減小因類型不一致致使的代碼質量問題。所以,Go 語言很是適合構建對穩定性靈活性均有要求的大型 IT 系統,這也是不少大型互聯網公司用 Golang 重構老代碼的重要緣由:傳統的靜態 OOP 語言(例如 Java、C#)穩定性高但缺少靈活性;而動態語言(例如 PHP、Python、Ruby、Node.js)靈活性強但缺少穩定性。所以,「熊掌和魚兼得」 的 Golang,受到開發者們的追捧是天然而然的事情,畢竟,「天下苦 Java/PHP/Python/Ruby 們久矣「。git

不過,Go 語言並非沒有缺點。用辯證法的思惟方式能夠推測,Golang 的一些突出特性將成爲它的雙刃劍。例如,Golang 語法簡單的優點特色將限制它處理複雜問題的能力。尤爲是 Go 語言缺少泛型(Generics)的問題,致使它構建通用框架的複雜度大增。雖然這個突出問題在 2.0 版本極可能會有效解決,但這也反映出來明星編程語言也會有缺點。固然,Go 的缺點還不止於此,Go 語言使用者還會吐槽其囉嗦的錯誤處理方式(Error Handling)、缺乏嚴格約束的鴨子類型(Duck Typing)、日期格式問題等。下面,咱們將從 Golang 語言特色開始,由淺入深多維度深刻分析 Golang 的優缺點以及項目適用場景。程序員

語言特色

簡潔的語法特徵

Go 語言的語法很是簡單,至少在變量聲明、結構體聲明、函數定義等方面顯得很是簡潔。github

變量的聲明不像 Java 或 C 那樣囉嗦,在 Golang 中能夠用 := 這個語法來聲明新變量。例以下面這個例子,當你直接使用 := 來定義變量時,Go 會自動將賦值對象的類型聲明爲賦值來源的類型,這節省了大量的代碼。golang

func main() {
valInt := 1  // 自動推斷 int 類型
valStr := "hello"  // 自動推斷爲 string 類型
valBool := false  // 自動推斷爲 bool 類型
}docker

Golang 還有不少幫你節省代碼的地方。你能夠發現 Go 中不會強制要求用 new 這個關鍵詞來生成某個類(Class)的新實例(Instance)。並且,對於公共和私有屬性(變量和方法)的約定再也不使用傳統的 publicprivate 關鍵詞,而是直接用屬性變量首字母的大小寫來區分。下面一些例子能夠幫助讀者理解這些特色。數據庫

// 定義一個 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 代碼中除了容易看花眼的多層花括號之外,還充斥着大量的 publicprivatestaticthis 等修飾用的關鍵詞,顯得異常囉嗦;而 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/awaitPromise 語法,甚至是 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 語言是靜態類型,相對於動態類型語言更穩定和可預測
  • IOP 鴨子類型比嚴格的 OOP 語言更簡潔
  • 沒有繼承、抽象、靜態、動態等特性
  • 缺乏泛型,致使靈活性下降
  • 難以快速構建複雜通用的框架或工具

錯誤處理

強制約束錯誤管理,避免 「try/catch 一把梭」

囉嗦的錯誤處理代碼,充斥着 if err := ...

編譯迅速

這絕對是一個優勢

怎麼多是缺點?

很是規依賴管理

  • 能夠直接引用發佈到 Github 上的倉庫做爲模塊依賴引用,省去了依賴託管的官方網站
  • 能夠隨時在 Github 上發佈 Go 語言編寫的第三方模塊
  • 自由的依賴發佈意味着 Golang 的生態發展將不受官方依賴託管網站的限制

嚴重依賴 Github,在 Github 上搜索 Go 語言模塊相對不精準

很是規日期格式

按照 6-1-2-3-4-5(2006-01-02 15:04:05),相對來講比較好記

對於已經習慣了 yyyy-MM-dd HH:mm:ss 格式的開發者來講很是不習慣

其實,每個特性在某種情境下都有其相應的優點和劣勢,不能一律而論。就像 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 語言中比較知名的後端框架有 GinBeegoEchoIris

固然,這裏並非說用 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 做爲實際工做開發語言的時候,須要全面瞭解其語言特性,從而作出最合理的技術選型。就像打網球同樣,不只須要掌握正反手,還要會發球、高壓球、截擊球等技術動做,這樣才能把網球打好。

社區

若是您對筆者的文章感興趣,能夠加筆者微信 tikazyq1 並註明 "碼之道",筆者會將你拉入 "碼之道" 交流羣。

相關文章
相關標籤/搜索