你應該知道Go語言的幾個優勢

騰訊雲加社群發表於2019-03-03

要說起GO語言的優勢,我們就得從GO語言的歷史講起了……

本文由騰訊技術工程官方號發表在騰訊雲+社群

2007年,受夠了C++煎熬的Google首席軟體工程師Rob Pike糾集Robert Griesemer和Ken Thompson兩位牛人,決定創造一種新語言來取代C++, 這就是Golang。出現在21世紀的GO語言,雖然不能如願對C++取而代之,但是其近C的執行效能和近解析型語言的開發效率以及近乎於完美的編譯速度,已經風靡全球。特別是在雲專案中,大部分都使用了Golang來開發,不得不說,Golang早已深入人心。而對於一個沒有歷史負擔的新專案,Golang或許就是個不二的選擇。

被稱為GO語言之父的Rob Pike說,你是否同意GO語言,取決於你是認可少就是多,還是少就是少(Less is more or less is less)。Rob Pike以一種非常樸素的方式,概括了GO語言的整個設計哲學--將簡單、實用體現得淋漓盡致。

很多人將GO語言稱為21世紀的C語言,因為GO不僅擁有C的簡潔和效能,而且還很好的提供了21世紀網際網路環境下服務端開發的各種實用特性,讓開發者在語言級別就可以方便的得到自己想要的東西。

發展歷史

2007年9月,Rob Pike在Google分散式編譯平臺上進行C++編譯,在漫長的等待過程中,他和Robert Griesemer探討了程式設計語言的一些關鍵性問題,他們認為,簡化程式語言相比於在臃腫的語言上不斷增加新特性,會是更大的進步。隨後他們在編譯結束之前說服了身邊的Ken Thompson,覺得有必要為此做一些事情。幾天後,他們發起了一個叫Golang的專案,將它作為自由時間的實驗專案。

2008年5月 Google發現了GO語言的巨大潛力,得到了Google的全力支援,這些人開始全職投入GO語言的設計和開發。

2009年11月 GO語言第一個版本釋出。2012年3月 第一個正式版本Go1.0釋出。

2015年8月 go1.5釋出,這個版本被認為是歷史性的。完全移除C語言部分,使用GO編譯GO,少量程式碼使用匯編實現。另外,他們請來了記憶體管理方面的權威專家Rick Hudson,對GC進行了重新設計,支援併發GC,解決了一直以來廣為詬病的GC時延(STW)問題。並且在此後的版本中,又對GC做了更進一步的優化。到go1.8時,相同業務場景下的GC時延已經可以從go1.1的數秒,控制在1ms以內。GC問題的解決,可以說GO語言在服務端開發方面,幾乎抹平了所有的弱點。

在GO語言的版本迭代過程中,語言特性基本上沒有太大的變化,基本上維持在GO1.1的基準上,並且官方承諾,新版本對老版本下開發的程式碼完全相容。事實上,GO開發團隊在新增語言特性上顯得非常謹慎,而在穩定性、編譯速度、執行效率以及GC效能等方面進行了持續不斷的優化。

開發團隊

img

GO語言的開發陣營可以說是空前強大,主要成員中不乏計算機軟體界的歷史性人物,對計算機軟體的發展影響深遠。Ken Thompson,來自貝爾實驗室,設計了B語言,創立了Unix作業系統(最初使用B語言實現),隨後在Unix開發過程中,又和Dennis Ritchie一同設計了C語言,繼而使用C語言重構了Unix作業系統。Dennis Ritchie和Ken Thompson被稱為Unix和C語言之父,並在1983年共同被授以圖靈獎,以表彰他們對計算機軟體發展所作的傑出貢獻。Rob Pike,同樣來自貝爾實驗室,Unix小組重要成員,發明了Limbo語言,並且和Ken Thompson共同設計了UTF-8編碼,《Unix程式設計環境》、《程式設計實踐》作者之一。

可以說,GO語言背靠Google這棵大樹,又不乏牛人坐鎮,是名副其實的“牛二代”。

img

大名鼎鼎的Docker,完全用GO實現,業界最為火爆的容器編排管理系統kubernetes,完全用GO實現,之後的Docker Swarm,完全用GO實現。除此之外,還有各種有名的專案如etcd/consul/flannel等等,均使用GO實現。有人說,GO語言之所以出名,是趕上了雲時代,但為什麼不能換種說法,也是GO語言促使了雲的發展?

除了雲專案外,還有像今日頭條、UBER這樣的公司,他們也使用GO語言對自己的業務進行了徹底的重構。

GO語言關鍵特性

GO語言之所以厲害,是因為它在服務端的開發中,總能抓住程式設計師的痛點,以最直接、簡單、高效、穩定的方式來解決問題。這裡我們並不會深入討論GO語言的具體語法,只會將語言中關鍵的、對簡化程式設計具有重要意義的方面介紹給大家,跟隨大師們的腳步,體驗GO的設計哲學。

GO語言的關鍵特性主要包括以下幾方面:

  • 併發與協程
  • 基於訊息傳遞的通訊方式
  • 豐富實用的內建資料型別
  • 函式多返回值
  • defer機制
  • 反射(reflect)
  • 高效能HTTP Server
  • 工程管理
  • 程式設計規範

img

在當今這個多核時代,併發程式設計的意義不言而喻。當然,很多語言都支援多執行緒、多程式程式設計,但遺憾的是,實現和控制起來並不是那麼令人感覺輕鬆和愉悅。Golang不同的是,語言級別支援協程(goroutine)併發(協程又稱微執行緒,比執行緒更輕量、開銷更小,效能更高),操作起來非常簡單,語言級別提供關鍵字(go)用於啟動協程,並且在同一臺機器上可以啟動成千上萬個協程。

對比JAVA的多執行緒和GO的協程實現,明顯更直接、簡單。這就是GO的魅力所在,以簡單、高效的方式解決問題,關鍵字go,或許就是GO語言最重要的標誌。

基於訊息傳遞的通訊方式

img

在非同步的併發程式設計過程中,只能方便、快速的啟動協程還不夠。協程之間的訊息通訊,也是非常重要的一環,否則,各個協程就會成為脫韁的野馬而無法控制。在GO語言中,使用基於訊息傳遞的通訊方式(而不是大多數語言所使用的基於共享記憶體的通訊方式)進行協程間通訊,並且將訊息管道(channel)作為基本的資料型別,使用型別關鍵字(chan)進行定義,併發操作時執行緒安全。這點在語言的實現上,也具有革命性。可見,GO語言本身並非簡單得沒有底線,恰恰他們會將最實用、最有利於解決問題的能力,以最簡單、直接的形式提供給使用者。

Channel並不僅僅只是用於簡單的訊息通訊,還可以引申出很多非常實用,而實現起來又非常方便的功能。比如,實現TCP連線池、限流等等,而這些在其它語言中實現起來並不輕鬆,但GO語言可以輕易做到。

img

GO語言作為編譯型語言,在資料型別上也支援得非常全面,除了傳統的整型、浮點型、字元型、陣列、結構等型別外。從實用性上考慮,也對字串型別、切片型別(可變長陣列)、字典型別、複數型別、錯誤型別、管道型別、甚至任意型別(Interface{})進行了原生支援,並且用起來非常方便。比如字串、切片型別,操作簡便性幾乎和python類似。

另外,將錯誤型別(error)作為基本的資料型別,並且在語言級別不再支援try…catch的用法,這應該算是一個非常大膽的革命性創舉,也難怪很多人吐槽GO語言不倫不類。但是跳出傳統的觀念,GO的開發者認為在程式設計過程中,要保證程式的健壯性和穩定性,對異常的精確化處理是非常重要的,只有在每一個邏輯處理完成後,明確的告知上層呼叫,是否有異常,並由上層呼叫明確、及時的對異常進行處理,這樣才可以高程度的保證程式的健壯性和穩定性。雖然這樣做會在程式設計過程中出現大量的對error結果的判斷,但是這無疑也增強了開發者對異常處理的警惕度。而實踐證明,只要嚴格按GO推薦的風格編碼,想寫出不健壯的程式碼,都很難。當然,前提是你不排斥它,認可它。

img

在語言中支援函式多返回值,並不是什麼新鮮事,Python就是其中之一。允許函式返回多個值,在某些場景下,可以有效的簡化程式設計。GO語言推薦的程式設計風格,是函式返回的最後一個引數為error型別(只要邏輯體中可能出現異常),這樣,在語言級別支援多返回值,就很有必要了。

Defer延遲處理機制

img

在GO語言中,提供關鍵字defer,可以通過該關鍵字指定需要延遲執行的邏輯體,即在函式體return前或出現panic時執行。這種機制非常適合善後邏輯處理,比如可以儘早避免可能出現的資源洩漏問題。

可以說,defer是繼goroutine和channel之後的另一個非常重要、實用的語言特性,對defer的引入,在很大程度上可以簡化程式設計,並且在語言描述上顯得更為自然,極大的增強了程式碼的可讀性。

img

Golang作為強型別的編譯型語言,靈活性上自然不如解析型語言。比如像PHP,弱型別,並且可以直接對一個字串變數的內容進行new操作,而在編譯型語言中,這顯然不太可能。但是,Golang提供了Any型別(interface{})和強大的型別反射(reflect)能力,二者相結合,開發的靈活性上已經很接近解析型語言。在邏輯的動態呼叫方面,實現起來仍然非常簡單。既然如此,那麼像PHP這種解析型語言相比於GO,優勢在那裡呢?就我個人而言,寫了近10年的PHP,實現過開發框架、基礎類庫以及各種公共元件,雖然執行效能不足,但是開發效率有餘;而當遇上Golang,這些優勢似乎不那麼明顯了。

img

作為出現在網際網路時代的服務端語言,面向使用者服務的能力必不可少。GO在語言級別自帶HTTP/TCP/UDP高效能伺服器,基於協程併發,為業務開發提供最直接有效的能力支援。要在GO語言中實現一個高效能的HTTP Server,只需要幾行程式碼即可完成,非常簡單。

img

在GO語言中,有一套標準的工程管理規範,只要按照這個規範進行專案開發,之後的事情(比如包管理、編譯等等)都將變得非常的簡單。

在GO專案下,存在兩個關鍵目錄,一個是src目錄,用於存放所有的.go原始碼檔案;一個是bin目錄,用於存在編譯後的二進位制檔案。在src目錄下,除了main主包所在的目錄外,其它所有的目錄名稱與直接目錄下所對應的包名保持對應,否則編譯無法通過。這樣,GO編譯器就可以從main包所在的目錄開始,完全使用目錄結構和包名來推導工程結構以及構建順序,避免像C++一樣,引入一個額外的Makefile檔案。

在GO的編譯過程中,我們唯一要做的就是將GO專案路徑賦值給一個叫GOPATH的環境變數,讓編譯器知道將要編譯的GO專案所在的位置。然後進入bin目錄下,執行go build {主包所在的目錄名},即可秒級完成工程編譯。編譯後的二進位制檔案,可以推到同類OS上直接執行,沒有任何環境依賴。

img

GO語言的程式設計規範強制整合在語言中,比如明確規定花括號擺放位置,強制要求一行一句,不允許匯入沒有使用的包,不允許定義沒有使用的變數,提供gofmt工具強制格式化程式碼等等。奇怪的是,這些也引起了很多程式設計師的不滿,有人發表GO語言的XX條罪狀,裡面就不乏對程式設計規範的指責。要知道,從工程管理的角度,任何一個開發團隊都會對特定語言制定特定的程式設計規範,特別像Google這樣的公司,更是如此。GO的設計者們認為,與其將規範寫在文件裡,還不如強制整合在語言裡,這樣更直接,更有利用團隊協作和工程管理。

API快速開發框架實踐

程式語言是一個工具,它會告訴我們能做什麼,而怎麼做會更好,同樣值得去探討。這部分會介紹用GO語言實現的一個開發框架,以及幾個公共元件。當然,框架和公共元件,其它語言也完全可以實現,而這裡所關注的是成本問題。除此之外,拋開GO語言本身不說,我們也希望可以讓大家從介紹的幾個元件中,得到一些解決問題的思路,那就是通過某種方式,去解決一個面上的問題,而非一味的寫程式碼,最終卻只是解決點上的問題。如果你認可這種方式,相信下面的內容也許會影響你之後的專案開發方式,從根本上提高開發效率。

我們為什麼選擇GO語言

選擇GO語言,主要是基於兩方面的考慮

  1. 執行效能 縮短API的響應時長,解決批量請求訪問超時的問題。在Uwork的業務場景下,一次API批量請求,往往會涉及對另外介面服務的多次呼叫,而在之前的PHP實現模式下,要做到並行呼叫是非常困難的,序列處理卻不能從根本上提高處理效能。而GO語言不一樣,通過協程可以方便的實現API的並行處理,達到處理效率的最大化。 依賴Golang的高效能HTTP Server,提升系統吞吐能力,由PHP的數百級別提升到數千裡甚至過萬級別。
  2. 開發效率 GO語言使用起來簡單、程式碼描述效率高、編碼規範統一、上手快。 通過少量的程式碼,即可實現框架的標準化,並以統一的規範快速構建API業務邏輯。 能快速的構建各種通用元件和公共類庫,進一步提升開發效率,實現特定場景下的功能量產。

img

很多人在學習一門新語言或開啟一個新專案時,都會習慣性的是網上找一個認為合適的開源框架來開始自己的專案開發之旅。這樣並沒有什麼不好,但是個人覺得,瞭解它內部的實現對我們會更有幫助。或許大家已經注意到了,所說的MVC框架,其本質上就是對請求路徑進行解析,然後根據請求路徑段,路由到相應的控制器(C)上,再由控制器進一步呼叫資料邏輯(M),拿到資料後,渲染檢視(V),返回使用者。在整個過程中,核心點在於邏輯的動態呼叫。

不過,對API框架的實現相對於WEB頁面框架的實現,會更簡單,因為它並不涉及檢視的渲染,只需要將資料結果以協議的方式返回給使用者即可。

使用GO語言實現一套完整的MVC開發框架,是非常容易的,整合HTTP Server的同時,整個框架的核心程式碼不會超過300行,從這裡可以實際感受到GO的語言描述效率之高(如果有興趣,可以參考Uwork開源專案seine)。

也有人說,在GO語言中,就沒有框架可言,言外之意是說,引入一個重型的開源框架,必要性並不大,相反還可能把簡單的東西複雜化。

img

在實際專案開發過程中,只有高效的開發語言還不夠,要想進一步將開發效率擴大化,不斷的沉澱公共基礎庫是必不可少的,以便將通用的基礎邏輯進一步抽象和複用。

除此之外,通用元件能力是實現功能量產的根本,對開發效率會是質的提升。元件化的開發模式會幫忙我們將問題的解決能力從一個點上提升到一個面上。以下會重點介紹幾個通用元件的實現,有了它們的存在,才能真正的解放程式設計師的生產力。而這些強有力的公共元件在Golang中實現起來並不複雜。同時,結合Golang的併發處理能力,相比於PHP的版本實現,執行效率也會有質的提升。這是元件能力和語言效率的完美結合。

img

通用列表元件用於所有可能的二維資料來源(如MySQL/MongoDB/ES等等)的資料查詢場景,從一個面上解決了資料查詢問題。在Uwork專案開發中,被大量使用,實現資料查詢介面和頁面查詢列表的量產開發。它以一個JSON配置檔案為中心,來實現對通用資料來源的查詢,並將查詢結果以API或頁面的形式自動返回給使用者。整個過程中幾乎沒有程式碼開發,而唯一要做的只是以一種統一的規範編寫配置檔案(而不是程式碼),真正實現了對資料查詢需求的功能量產。

img

以上是通用列表元件的構建過程,要實現這樣一個功能強大的通用元件,是不是會給人一種可望而不可及的感覺?其實並非如此,只要理清了它的整個過程,將構建思路融入Golang中,並不是一件複雜的事情。在我們的專案中,整個元件的實現,只用了不到700行Go程式碼,就解決了一系列的資料查詢問題。另外,通過Golang的併發特性,實現欄位處理器的並行執行,進一步的提高了元件的執行效率。可以說,通用列表和Golang的融合,是效能和效率的完美結合。

img

通用表單元件主要用於對資料庫的增、刪、改場景。該元件在Uwork的專案開發中,也有廣泛的應用,與通用列表類似,以一個JSON配置檔案為中心,來完成對資料表資料的增、刪、改操作。特別是近期完成的部件級SDB管理平臺,通過通用表單實現了對整個系統的資料維護,通過高度抽象化,做到了業務的無程式碼化生產。

img

以上是通用表單的完整構建過程,而對於這個一個元件的實現,我們用了不到1000行的GO程式碼,就解決了對資料表資料維護整個面上的問題。

img

GO語言本身支援協程併發,協程非常輕量,可以快速啟動成千上萬個協程工作單元。如果對協程任務的數量控制不當,最後的結果很可能適得其反,從而對外部或本身的服務造成不必要的壓力。協程池可以在一定程度上控制執行單元的數量,保證執行的安全性。而在Golang中要實現這樣一個協程池,是非常簡單的,只需要對channel和goroutine稍加封裝,就可以完成,整個構建過程不到80行程式碼。

img

在API開發過程中,資料校驗永遠是必不可或缺的一個環節。如果只是簡單的資料校驗,幾行程式碼也許就完成了,可是當遇上覆雜的資料校驗時,很可能幾百行的程式碼量也未必能完成,特別是遇到遞迴型別的資料校驗,那簡直就是一個噩夢。

資料校驗元件,可以通過一種資料模板的配置方式,使用特定的邏輯來完成通用校驗,開發者只需要配置好相應的資料模板,進行簡單的呼叫,即可完成整個校驗過程。而對於這樣一個通用性的資料校驗元件,在GO語言中只用了不到700行的程式碼量就完成了整個構建。

小結

img

在實際專案開發過程中,對開發效率提升最大的,無疑是符合系統業務場景的公共元件能力,這點也正好應證了Rob Pike那句話(Less is lessor Less is more),真正的高效率開發,是配置化的,並不需要寫太多的程式碼,甚至根本就不需要寫程式碼,即可完成邏輯實現,而這種方式對於後期的維護成本也是最優的,因為做到了高度的統一。

GO的語言描述效率毋庸置疑,對上述所有公共元件的實現,均未超過1000行程式碼,就解決了某個面上的問題。

(以上的部分程式碼已經在Uwork開源專案seine中提供)

效能評測

壓力測試環境說明:

  • 服務執行機器:單臺空閒B6,24核CPU、64G記憶體。
  • PHP API環境:Nginx+PHP-FPM,CI框架。其中Nginx啟動10個子程式,每個子程式最大接收1024個連線,php-fpm使用static模式,啟動2000個常駐子程式。
  • Golang API環境:使用go1.8.6編譯,直接拉起Golang API Server程式(HttpServer),不考慮調優。
  • 客戶發起請求測試程式:使用Golang編寫,協程併發,執行在獨立的另外一臺空閒B6上,24核CPU,64G記憶體,依次在1-2000個不同級別(併發數步長為50)的併發上分別請求20000次。

壓力測試結果對比

img

在Golang API框架中,當併發數>50時,處理QPS在6.5w/s附近波動。表現穩定,壓力測試過程無報錯。

Nginx+php-fpm,只在index.php中輸出exit('ok'),當併發數>50時,處理QPS在1w/s附近波動。表現穩定,壓力測試過程無報錯。

Nginx+php-fpm+CI框架中,邏輯執行到具體業務邏輯點,輸出exit('ok'),當併發數>50時,處理QPS在750/s附近波動。並且表現不穩定,壓力測試過程中隨著併發數的增大,錯誤量隨之增加。

通過壓力測試可以發現,Golang和PHP在執行效能上,並沒有什麼可比性;而使用Golang實現的HTTP API框架,空載時單機效能QPS達到6.5w/s,還是非常令人滿意的。

開發過程中需要注意的點

以下是在實際開發過程中遇到的一些問題,僅供參考:

異常處理統一使用error,不要使用panic/recover來模擬throw…catch,最初我是這麼做的,後來發現這完全是自以為是的做法。

原生的error過於簡單,而在實際的API開發過程中,不同的異常情況需要附帶不同的返回碼,基於此,有必要對error再進行一層封裝。

任何協程邏輯執行體,邏輯最開始處必須要有defer recover()異常恢復處理,否則goroutine內出現的panic,將導致整個程式宕掉,需要避免部分邏輯BUG造成全域性影響。

在Golang中,變數(chan型別除外)的操作是非執行緒安全的,也包括像int這樣的基本型別,因此併發操作全域性變數時一定要考慮加鎖,特別是對map的併發操作。

所有對map鍵值的獲取,都應該判斷存在性,最好是對同類操作進行統一封裝,避免出現不必要的執行時異常。

定義slice資料型別時,儘量預設長度,避免內部出現不必要的資料重組。


此文已由作者授權騰訊雲+社群釋出,原文連結:https://cloud.tencent.com/developer/article/1145176?fromSource=waitui

歡迎大家前往騰訊雲+社群或關注雲加社群微信公眾號(QcloudCommunity),第一時間獲取更多海量技術實踐乾貨哦~

相關文章