大道至簡—GO語言最佳實務 - 雲+社區

來源:互聯網
上載者:User

導讀: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世紀互連網環境下服務端開發的各種實用特性,讓開發人員在語言層級就可以方便的得到自己想要的東西。

本文大綱:

  • GO語言的發展與現狀
  • 發展曆史
  • Team Dev
  • 業務案例
  • 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的基準上,並且官方承諾,新版本對老版本下開發的代碼完全相容。事實上,GOTeam Dev在新增語言特性上顯得非常謹慎,而在穩定性、編譯速度、執行效率以及GC效能等方面進行了持續不斷的最佳化。

Team Dev

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

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

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

GO語言關鍵特性

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

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

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

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

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

基於訊息傳遞的通訊方式

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

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

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

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

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

Defer延遲處理機制

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

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

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

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

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

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

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

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

API快速開發架構實踐

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

我們為什麼選擇GO語言

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

小結

在實際項目開發過程中,對開發效率提升最大的,無疑是符合系統業務情境的公用組件能力,這點也正好應證了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資料類型時,盡量預設長度,避免內部出現不必要的資料重組。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.