解讀 2018之Go語言篇(上):爲什麼Go語言越來越熱?

2018年接近尾聲,InfoQ 策劃了“解讀 2018”年終技術盤點系列文章,希望能夠給讀者清晰地梳理出重要技術領域在這一年來的發展和變化。本篇文章是Go語言2018年終盤點,分爲上下兩篇,客觀、深入分析2018年Go語言的技術發展現狀,同時對明年可能的發展情況進行預測和展望。

今年真可謂是不平靜的一年,前有人工智能國家級戰略的發佈,行業已經在大跨步的挺進,但人才缺口每天都在擴大;後有區塊鏈技術從爆發式增長到大幅回落,無數程序員蜂擁而至,又在現如今變得手足無措。

那麼,Go語言在2018年這一年發展得又如何呢?它的下一步又將會怎樣?且聽筆者細細道來。

首先,筆者要說的是,在TIOBE於2018年11月份公佈的編程語言排行榜中,Go語言已然擠到了前10的位置。雖然這與去年同期的第14位看起來相差不大,但卻是一個里程碑式的進步。

圖1:TIOBE Index for Nov 2018

從Google Trends提供的流行趨勢統計來看,在過去的12個月裏,Go語言的流行也是持續升溫的。

圖2: Google Trends - Golang熱度隨時間變化的趨勢

這種升溫雖然並不算快,但是很持久。這對編程語言的生態環境和人才的發展是非常有利的。

此外,完全不出乎我們的意料:中國依然是Go語言愛好者最多的國家,沒有之一。

圖3: Google Trends - Golang按區域顯示的搜索熱度

具有諷刺意味的是,作爲Go語言誕生地的美國,僅排在了第15位。我們對先進技術和前沿科技的熱衷絕對是不輸他國的。下面,讓我們再把尺度縮小到城市級別。

圖4 :Google Trends - Golang按區域顯示的搜索熱度(城市)

顯然,在我國,北京、深圳、上海這三個城市聚集了非常多的Go語言程序員和工程師。尤其是北京,簡直是Go語言愛好者的聖地啊!

至於北京博得頭籌的原因,據筆者觀察,首先肯定是:在北京的互聯網公司很多,起碼明顯多於其他的一、二線城市。Go語言如今在互聯網公司中非常流行,即使有的公司高層並沒有批准大規模地使用Go語言,但是工程師們都在做積極的嘗試。

其次,北京做雲計算的公司很多,不論是面向市場的公有云還是自建自用的私有云。說到雲計算,我們就不得不提及開放平臺技術、容器技術、集羣管理技術,以及現在很火熱的微服務(Microservices)和Serverless技術,等等。而這些,恰恰都是Go語言的專長。在這些方面,有很多成熟的基於Go語言的解決方案可供選擇。

再次,北京的高科技創業公司非常多。他們往往沒有歷史包袱、勇於創造和嘗試。在做技術選型的時候,他們也更容易選擇Go語言。因爲,Go語言既擁有編譯型編程語言固有的高運行效率,又具有解釋型編程語言常有的高開發效率。而且,Go語言還不像有些編程語言那樣時不時地出現內鬥、分裂等混亂情況,當然也沒有無良的技術持有者吵鬧着要對編程語言的商用進行收費。

Go語言在語言規範的發展、版本的迭代和開發者生態的建設方面都非常的穩定,並有着良好的包容性和兼容性。保持簡單、面向契約和利於協作是Go語言最突出的設計哲學。無論是做軟件原型,還是用於小團隊作戰,又或是進行大規模的研發,Go語言都會是很不錯的選擇。

最後,很多喜愛Go語言、致力於推廣Go語言技術的個人開發者、技術團隊、互聯網公司以及知識服務廠商也都在北京。這都直接或間接地導致了Go語言在這座城市的流行。

好了,到這裏,筆者相信你已經對Go語言在中國的流行有了一定的瞭解。下面,我們再來說說Go語言在2018年具體都有哪些進展。

首先說一下,關於Go語言在2018年之前的具體進展,筆者推薦你去看這幾篇同系列文章,如下:

語法和平臺

Go語言官方團隊在2018年2月正式發佈它的1.10版本。不同於其他很多被稱爲版本帝的編程語言,到了這樣一個版本號10,Go 1在語言規範方面已經幾乎沒有什麼改動了,一些語法上的小小增強也並不值得我們特別關注。而在2018年8月發佈的Go 1.1更是沒有任何語言規範方面的變動。

Go語言對於本身的向後兼容性保持得非常好,高版本對低版本中的語言語法、工具和標準庫都不會有任何破壞。然而,Go語言在其支持的操作系統方面還是很大刀闊斧的。這體現在,Go 1.10不再支持10.3以下版本的FreeBSD和8.0以下版本的NetBSD。並且,這個版本也是支持OpenBSD 6.0、OS X 10.9以及Windows XP和 Windows Vista的最後一個版本。**在這些操作系統之上編寫或運行Go語言程序的開發者們要注意。

環境和工具

使用過Go語言的開發者們都知道,當把Go語言的預編譯包解壓到某個目錄後,我們還需要至少設置兩個環境變量——GOROOT和GOPATH。前者代表直接包含Go語言本身的那個目錄路徑,而後者則用於指定可放置第三方庫和自有代碼的工作區(或者說工作目錄)的路徑。

一個好消息是,自Go語言的1.10版本起,GOROOT這個環境變量就沒有必要設置了。如果我們不設置它,那麼Go的標準工具會嘗試以自身所在的目錄爲基礎,自動地推斷出GOROOT應該指向的目錄路徑。

另外,從這個版本開始,我們可以自行地設定Go語言的臨時目錄路徑了,設定的途徑是設置環境變量GOTMPDIR。Go語言的臨時目錄主要用於存放Go工具在編譯或測試程序時產生的各種臨時文件。在這之前,這些臨時文件都會被存放到固定的地方,此地的具體路徑會根據操作系統的不同而不同,一般會位於操作系統的臨時目錄的某個子目錄下。自定義這個目錄的好處在於,可以讓我們方便地觀察編譯過程,並查看編譯或測試的中間結果。

說到編譯,筆者一定要提一下1.10版本的另一項改進,這與go build命令有關。以前,如果我們要強行地重新構建所有相關的代碼包,那麼就需要在運行這個命令的時候追加標記“-a”。而現在,我們無需這樣做了。go build命令會根據源碼文件內容、構建標記和編譯元數據,自動地決定什麼時候應該重新構建那些代碼包。這項工作再也不需要人工干預了。

與此項改進相關的變化是,go build命令現在總是會把最近的構建結果緩存起來,以便在將來的構建中重用。我們可以通過運行go env GOCACHE命令來查看緩存目錄的路徑。緩存的數據總是能夠正確地反映出當時的源碼文件、構建環境和編譯器選項等的真實情況。一旦有任何變動,緩存數據就會失效,go build命令就會再次真正地執行構建。因此,我們並不用擔心緩存數據體現的不是實時的結果。實際上,這正是上述改進能夠有效的主要原因。go build命令會定期地刪除最近未使用的緩存數據,但如果你想手動刪除所有的緩存數據,運行一下go clean -cache命令就好了。

順便說一下,對於測試成功的結果,go命令也是會緩存的。運行go clean -testcache命令將會刪除掉所有的測試結果緩存。不過別擔心,這樣做肯定不會刪除任何的構建結果緩存,它們是兩碼事。

此外,設置環境變量GODEBUG的值也可以稍稍地改變go命令的緩存行爲。比如,設置值爲gocacheverify=1將會導致go命令繞過任何的緩存數據,而真正地執行操作並重新生成所有結果,然後再去檢查新的結果與現有的緩存數據是否一致。

再來說go install命令。現在,go install命令在默認情況下只會去安裝我們明確指定的那些代碼包。這些代碼包依賴的那些包並不會被安裝。這同樣得益於構建結果緩存,它可以使安裝的速度得到明顯的提升。如果你想要強制地安裝依賴包,那麼請在運行命令的時候追加“-i”標記。

程序測試

前面我們說過了,測試成功的結果也會被緩存。如果go test命令確定可以使用被緩存的結果,那麼它打印出的內容也會出自於緩存。這時,被打印的內容中會包含“(cached)”字樣。

另外,go test命令現在會自動地運行go vet命令,以便在真正運行測試之前識別出一些程序編寫方面的問題。我們都知道,go vet命令用於對Go語言源碼進行靜態檢查,並報告已發現的可疑問題。這些問題一般都是符合語法規則的,因此編譯器無法查出它們。但是,它們很有可能代表了對某些程序實體(或者說API)的錯誤使用。雖然go vet命令有時候並不能保證它報告的每一個問題都是真正的問題,但它卻可以給予我們一份重要的參考,以便讓我們在編程的過程中小心行事。

與Go語言提供的很多高級功能一樣,我們也可以阻止go test命令自動運行go vet命令,這需要在運行前者的時候追加“-vet=off”這個標記。

最後,關於go test命令,還有兩個值得注意的新標記——“-failfast”和“-json”。顧名思義,“-failfast”標記可以讓go test命令一旦發現有測試失敗的情況就立即忽略掉剩餘的測試並終止運行。不過要注意,如果存在與失敗的測試併發進行的測試的話,那麼後者還是會繼續運行直至完成的。“-json”標記對於程序測試的自動化大有裨益。它會讓go test命令產生JSON格式的測試報告,這使得其他程序很容易讀入和處理。

程序文檔

關於程序文檔,只有一點需要我們注意。**Go 1.11是godoc命令支持命令行接口的最後一個版本。**在未來的版本中,我們運行godoc命令的時候,它會啓動一個Web服務器,以便讓我們直接進入圖形化界面進行文檔查詢。

程序性能分析

現在,runtime/pprof代碼包中的Lookup函數已經支持了更加多樣的參數值。這就意味着,Go語言的程序性能分析現在可以生成和解讀更多視角下的分析報告了。我們可以把這樣的分析報告包含的內容叫做程序性能概要信息(簡稱概要信息),並把存儲這些分析報告的文件叫做概要文件。

Lookup函數可以生成的概要信息目前共有6種。這6種概要信息分別由字符串類型的參數值goroutineheapallocsthreadcreateblockmutex代表。下面是它們代表的含義:

  • goroutine:收集當前正在使用的所有goroutine的堆棧跟蹤信息。
  • heap:收集與堆內存的分配和釋放有關的採樣信息,默認以在用空間(inuse_space)的視角呈現。
  • allocs:同樣收集與堆內存的分配和釋放有關的採樣信息,但默認以已分配空間(alloc_space)的視角呈現。
  • threadcreate:收集一些特定的堆棧跟蹤信息,其中的調用鏈上的代碼都導致了新的操作系統線程的產生。
  • block:收集因爭用同步原語而被阻塞的那些代碼的堆棧跟蹤信息。
  • mutex:曾經作爲同步原語持有者的那些代碼的堆棧跟蹤信息。

這裏所說的同步原語,指的是存在於Go語言運行時系統內部的一種底層同步工具,或者說一種同步機制。它是直接面向內存地址的,並以異步信號量和原子操作作爲實現手段。我們已經熟知的通道、互斥鎖、條件變量、“WaitGroup”以及Go語言運行時系統本身,都會利用它來實現自己的功能。

另外,在用空間和已分配空間的區別是,前者指的是已經分配但還沒有被回收的空間,而後者只關注分配出的空間,不論它們是否已經被回收。

注意,如果我們在運行go test命令的時候追加了標記“-memprofile”,那麼該命令會通過底層的API以allocs爲視角生成概要信息和概要文件。這相當於對從測試開始時的所有已分配字節進行記錄,包含已經被垃圾回收器收回的那些字節。在Go 1.11版本之前,go test命令在這種情況下采用的是heap視角。

最後,go tool pprof工具已經可以正確地單獨讀取和處理所有種類的概要文件了。這得益於,從Go 1.10版本開始,blockmutex視角下的概要信息已經完善。在這之前,我們使用go tool pprof查閱這兩種概要文件的時候,還不得不同時指定相應程序的二進制文件。

運行時系統

需要特別注意,runtime代碼包中的LockOSThread函數和UnlockOSThread函數的行爲已經發生了變化。我們都知道,前一個函數的功能是將當前的goroutine與那一時刻正在承載這個goroutine運行的操作系統線程進行綁定。在綁定之後,這個goroutine就只能由該操作系統線程運行了,反之,該操作系統線程也只能運行這一個goroutine了。顯而易見,runtime. UnlockOSThread函數的功能是解除上述綁定關係。當然了,這兩個函數都只能作用於它們被調用時所在的那個goroutine。

以前,runtime. LockOSThread函數是冪等的。也就是說,無論我們在同一個goroutine中調用了它多少次,都只相當於調用了一次。另一方面,只要我們調用一次runtime. UnlockOSThread函數,就總是能夠解除針對於當前goroutine的這種綁定。

但是,從Go語言的1.10版本開始,在我們想要完全解除綁定的時候,可能就需要調用多次runtime. UnlockOSThread函數才能夠實現了。至於具體需要調用多少次完全取決於,當初在同一個goroutine中調用runtime. LockOSThread函數的次數。換句話說,只有進行相同次數的函數調用,才能讓當前goroutine與某個操作系統線程之間的綁定關係完全解除。我們可以把現在的這種對應關係理解爲是基於嵌套的,可以想象一下:當初包裝了多少層紙箱,現在就要拆開多少層紙箱。

其實一直以來,有很多第三方Go語言庫的作者都誤以爲對於這兩個函數的調用就是基於嵌套關係的。不過無論怎樣,我們現在都應該仔細檢查代碼並小心的應對了。

筆者認爲,如果你確實需要進行這種綁定,那麼就應該基於這兩個函數封裝一個數據結構。在這個數據結構中,至少應該包含一個用於記錄調用runtime. LockOSThread函數次數的字段,以方便後續的解綁操作。

在2018年,對於Go語言的運行時系統來說,我們可以輕易感知到的變化基本上只有這一個。不過,非常多的改進和優化都在悄無聲息的進行着,有的已經完成了,而有的還在進展之中。已完成的改進如:在通常情況下,我們傳遞給runtime.GOMAXPROCS函數的參數值已經不再受限了,只要它在int32類型可容納的範圍之內就可以。

標準庫

在Go語言的1.10和1.11這兩個版本中,官方團隊與社區開發者們一起對標準庫做了大量的改進。可喜可賀,社區開發者對Go語言的貢獻次數現在已經超過官方團隊了!

由於這方面的改進繁多,也由於筆者在新近發佈的極客時間專欄《Go語言核心36講》中已經詳細講解了不少,所以這裏就不再贅述了。

兩個新實驗

我們再來說說Go 1.11的兩個新實驗吧,一個是對WebAssembly的實驗性支持,另一個是推出由dep和vgo演化而來的依賴管理機制和新概念module。

按照官方的描述,WebAssembly(縮寫爲WASM)是一種二進制指令格式,它針對的是以堆棧爲基礎的虛擬機。WASM有很好的可移植性,以便讓C++、Golang、Rust等高級編程語言來操控它,並有能力部署到Web程序上。

用普通話來說,WASM提供了一種途徑,可以讓我們用後端編程語言直接去編寫Web頁面中的邏輯。在Go 1.11中,我們可以很輕易地把Go語言源碼文件轉換爲WASM格式的文件,然後在Web頁面中通過寥寥幾行JavaScript代碼引用這個文件並把其中的邏輯發佈到頁面上。WASM的1.0版本現在已經支持了絕大多數的主流網絡瀏覽器,比如:Chrome、Firefox、Safari等。如果想了解具體的玩法,你可以參看這個wiki頁面

筆者對Go語言官方的這種探索性實驗一直都持贊成的態度,不論是前些年的移動端(Android和iOS)方向,還是今年的Web端(WASM)方向。不過,筆者依然覺得Go語言的優勢在服務端,現在很明顯,而且在可預見的未來也應該是如此。所以,對於這些多端探索,筆者建議大家“保持關注,積極試驗,但不要偏移重心”。

相比之下,筆者倒是更加看好Go語言新放出的依賴管理機制。Go語言愛好者們都知道,Go語言在這方面一直是缺失的。雖然目前存在幾個不錯的第三方解決方案,但是沒有一個是可以脫穎而出的,同時官方也一直沒有給出一個統一的標準。

經過了一段時間的試驗和演化,Go語言官方的依賴管理機制終於脫胎於dep和vgo。雖然其間存在一些摩擦和風波,但是結果終歸是積極的。

在Go語言新的依賴管理機制中,module是一個非常重要的概念。簡單來說,module象徵着由某個Go語言代碼包以及它依賴的代碼包共同組成的一個獨立單元。這裏的Go語言代碼包和它依賴的那些代碼包都是版本化的。一個module的根目錄下總是直接存有一個名爲go.mod的文件。這個文件中會包含當前module的路徑,以及它依賴的那些module的路徑和版本號。如此一來,對於每一個版本的module,它依賴的所有代碼都會被固化下來。這對於後續的版本管理和module重建來說都是重要的基礎。詳情可以參看這裏的wiki頁面

不過,不要忘了,Go 1.11中包含的這個依賴管理機制是實驗性的。其中的任何部分都有可能由於社區的反饋和官方的改進而變化。所以,你在正式使用它之前一定要考慮到後續可能存在的變更成本。雖然如此,筆者仍然會鼓勵廣大開發者們去積極使用和反饋。想象一下maven對於Java世界的重要性吧。筆者相信,我們心目中的Go項目依賴管理機制已經離此不遠了。

參考文獻

[1] Go 1.10 is released: https://blog.golang.org/go1.10

[2] Go 1.11 is released: https://blog.golang.org/go1.11

[3] Diagnostics: https://golang.google.cn/doc/diagnostics.html

[4] WebAssembly: https://github.com/golang/go/wiki/WebAssembly

[5] Modules: https://github.com/golang/go/wiki/Modules

[6] Go 1.12 Release Notes(DRAFT): https://tip.golang.org/doc/go1.12

[7] Nine years of Go: https://blog.golang.org/9years

[8] Toward Go 2: https://blog.golang.org/toward-go2

[9] Go 2 Draft Designs: https://go.googlesource.com/proposal/+/master/design/go2draft.md


作者簡介
郝林,國內知名的Go語言技術佈道者,GoHackers技術社羣的發起人和組織者。他也是極客時間專欄《Go語言核心36講》的作者,以及圖靈原創圖書《Go併發編程實戰》的作者。他曾在輕鬆籌任大數據負責人,同時負責大數據部門和主站的後端技術團隊。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章