06.11 大道至簡—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 lessis less)。Rob Pike以一種非常樸素的方式,概括了GO語言的整個設計哲學--將簡單、實用體現得淋漓盡致。

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

本文大綱:

  • GO語言的發展與現狀

  • 發展歷史

  • 開發團隊

  • 業務案例

  • GO語言關鍵特性

  • 併發與協程

  • 基於消息傳遞的通信方式

  • 豐富實用的內置數據類型

  • 函數多返回值

  • Defer延遲處理機制

  • 反射(reflect)

  • 高性能HTTP Server

  • 工程管理

  • 編程規範

  • API快速開發框架實踐

  • 我們為什麼選擇GO語言

  • API框架的實現

  • 公共組件能力

  • 通用列表組件

  • 通用表單組件

  • 協程池

  • 數據校驗

  • 小結

  • 性能評測

  • 開發過程中需要注意的點

GO語言的發展與現狀

發展歷史

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性能等方面進行了持續不斷的優化。

開發團隊

大道至簡—GO語言最佳實踐

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這棵大樹,又不乏牛人坐鎮,是名副其實的“牛二代”。

大道至簡—GO語言最佳實踐

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

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

GO語言關鍵特性

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

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

  • 併發與協程

  • 基於消息傳遞的通信方式

  • 豐富實用的內置數據類型

  • 函數多返回值

  • defer機制

  • 反射(reflect)

  • 高性能HTTP Server

  • 工程管理

  • 編程規範

大道至簡—GO語言最佳實踐

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

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

基於消息傳遞的通信方式

大道至簡—GO語言最佳實踐

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

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

大道至簡—GO語言最佳實踐

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

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

大道至簡—GO語言最佳實踐

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

Defer延遲處理機制

大道至簡—GO語言最佳實踐

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

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

大道至簡—GO語言最佳實踐

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

大道至簡—GO語言最佳實踐

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

大道至簡—GO語言最佳實踐

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

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

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

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

API快速開發框架實踐

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

我們為什麼選擇GO語言

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

  • 執行性能

    縮短API的響應時長,解決批量請求訪問超時的問題。在Uwork的業務場景下,一次API批量請求,往往會涉及對另外接口服務的多次調用,而在之前的PHP實現模式下,要做到並行調用是非常困難的,串行處理卻不能從根本上提高處理性能。而GO語言不一樣,通過協程可以方便的實現API的並行處理,達到處理效率的最大化。

    依賴Golang的高性能HTTP Server,提升系統吞吐能力,由PHP的數百級別提升到數千裡甚至過萬級別。

  • 開發效率

    GO語言使用起來簡單、代碼描述效率高、編碼規範統一、上手快。

    通過少量的代碼,即可實現框架的標準化,並以統一的規範快速構建API業務邏輯。

    能快速的構建各種通用組件和公共類庫,進一步提升開發效率,實現特定場景下的功能量產。

大道至簡—GO語言最佳實踐

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

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

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

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

大道至簡—GO語言最佳實踐

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

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

大道至簡—GO語言最佳實踐

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

大道至簡—GO語言最佳實踐

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

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

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

大道至簡—GO語言最佳實踐

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

大道至簡—GO語言最佳實踐

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

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

小結

大道至簡—GO語言最佳實踐

在實際項目開發過程中,對開發效率提升最大的,無疑是符合系統業務場景的公共組件能力,這點也正好應證了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次。

壓力測試結果對比

在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數據類型時,儘量預設長度,避免內部出現不必要的數據重組。


分享到:


相關文章: