Google 的 Go 語言團隊會在 2018 年初宣佈 Go 1.10 正式版的發佈。10 這個二位數也預示着 Go 語言的進一步成熟。聽說,Go 2 也被提上了日程。Go 2 將會是大神們對 Go 語言的一次完全反思和改進。雖然如今細節尚未被暴露出來,可是這已經足以讓 Gopher(Go 語言愛好者)們激動不已了。會有泛型支持嗎?GC 會變革嗎?詳細調參會可行嗎?各類猜想已經在各個論壇和羣組裏層出不窮了。git
不過,飯要一口一口吃,肌肉要一點一點練。在憧憬將來以前,先讓咱們看看 Go 語言在 2017 年的表現。github
首先,根據 Google Trends 的統計結果(https://trends.google.com/trends/explore?q=golang&hl=en-US),咱們能夠看到 Go 語言在過去一年中的流行程度是穩中有升。golang
圖 1 Go 語言在 2017 年的流行趨勢算法
初看起來,Go 語言在 2017 年表現得比較平淡。可是,讓咱們把時間線拉長。docker
圖 2 Go 語言在過去 5 年間的流行趨勢編程
你必定看到了,Go 語言在 2017 年的「上升」是對近年來的一種延續。有些編程語言會隨着其擅長領域的爆紅或遭冷而大起大落。可是 Go 語言不會。這就像它樸素的編程風格同樣。它的使用範圍很廣,我幾乎能夠在任何場景下使用它。後端
Go 語言的適用範圍一直在不斷地擴大。通過廣大開發者的共同努力,它已開始涉足在當前大熱的數據科學和機器學習領域。雖然還只是開始,可是就像我在《Go 併發編程實戰》第 2 版的扉頁中說的那樣,我「深信 Go 語言在人工智能時代和機器人時代也能大放異彩」。安全
更使人欣慰的是,中國的開發者對於 Go 語言的流行起着相當重要的做用。在過去的一年裏,咱們的熱衷和貢獻依然領跑全世界!微信
圖 3 Go 語言在 2017 年的流行區域熱圖數據結構
回 顧
我在前年和去年分別寫過兩篇相關的文章——《解讀 2015 之 Golang 篇:Golang 的全迸發時代》和《解讀 2016 之 Golang 篇:極速提高,逐步超越》。你們能夠把它們當作一個系列的記錄參看。
類型別名
類型別名(type aliases)本來是要在 Go 1.8 發佈時推出的。可是因爲一些爭議和實現上的問題,Go 團隊把它推遲到了 Go 1.9。
這一特性實際上是爲開發者們的代碼庫重構準備的。代碼重構是對代碼的從新組織,以及這種重組與代碼包之間的關係的從新思考和修改過程。代碼重構的緣由多是代碼的拆分、命名優化或者依賴關係清理,等等。可是不論緣由是什麼,咱們的目的都是讓代碼變得更清楚、更易於使用,以及更容易擴展。
也許你有過這樣的經歷:在進行(範圍比較廣的)代碼重構的過程當中,原有代碼已經改變。甚至,當你完成重構並想把代碼合併回去的時候,原有代碼已經面目全非。合併代碼有時候比重構代碼更加困難。
這就引出了一個比較先進的重構方法——漸進式代碼重構。也就是說,同時計劃重構的終極目標和階段性目標。在達到完備狀態以前,設置幾個易實施而且可用的中間狀態。縱觀 Go 語言的代碼更新和版本升級過程,咱們不難發現 Go 團隊對這種方法的合理運用。
然而,在漸進式代碼重構的過程當中咱們也可能會遇到問題。例如,當你想把一個容許包外代碼訪問的類型遷移到另一個包中時,該怎麼作?簡單來講,應該分爲三步:1)在目的包中聲明一個名稱和功能都相同的新類型。2)檢查全部可能引用原類型的地方,並把那些引用都指向新類型。3)刪除原包中的那個類型。若是這個類型只被由你掌控的程序中引用,那麼這種方式固然沒問題。可是,若是該類型所在的是一個已被普遍依賴的底層代碼包,那我勸你仍是不要執行第 3 步了。除非你冒着被口水淹死的風險發佈程序不兼容聲明。但是不執行第 3 步就等於任由廢棄代碼在程序中蔓延。這也是很噁心的一件事。
講了這麼多,我要說的重點是:Go 的類型別名就是爲咱們解決這類兩難的問題的。在類型別名真正問世以前,Go 團隊本身在作上述重構時都不得不用上一些特殊的、脆弱的手段。
若使用類型別名的話,應該怎麼作?若是咱們要把 oldpkg.OldType 類型遷移到 newpkg 代碼包中並將其更名爲 NewType,那麼最少用 2 步就能夠完成:1)聲明 newpkg.NewType 類型。2)把 oldpkg.OldType 類型的聲明改成:
package oldpkg type OldType = newpkg.NewType
新的 oldpkg.OldType 類型聲明可使它與 newpkg.NewType 徹底等價,並能夠實現互換。也就是說,若是一個函數有一個 oldpkg.OldType 類型的參數聲明,那麼該函數就能夠接受一個 newpkg.NewType 類型的參數值。
固然,爲了避免留廢棄代碼你仍然須要在某個時間刪除掉 oldpkg.OldType。可是類型別名給了你和其餘使用 oldpkg.OldType 的開發者一個能夠遊刃有餘的必要條件。大家爲重構代碼而設置的中間狀態均可以輕鬆達成。
舉個例子,你在按照上述 2 步遷移完類型以後,能夠立刻把本身寫的某個函數的聲明由
package handler import oldpkg func HandleXXX(obj oldpkg.OldType){}
改成
package handler import newpkg func HandleXXX(obj newpkg.NewType){}
然而,在調用該函數的(其餘人寫的)程序那裏,卻能夠不用作任何修改(雖然最後可能要修改)。代碼 handler.HandleXXX(oldTypeVar) 仍然有效。其中的 oldTypeVar 是 oldpkg.OldType 類型的值。這就至關於可讓各方按照本身的節奏重構代碼了。
再後面的情景多是:你在你的代碼包仍是 1.0 版本的時候就發佈聲明說 oldpkg.OldType 類型即將在 2.0 版本中刪除。而當你在開發 2.0 版本時,問心無愧地刪掉了 oldpkg.OldType。
實際上,類型別名只是 Go 團隊爲了讓咱們順利實施大規模 Go 軟件工程的舉措之一。他們一直在致力於幫助開發者們高效地編寫 Go 代碼和利用 Go 代碼包,並避免因不經意的重複造輪子而致使的代碼膨脹。若是你真正用過 Go 語言,那麼也必定能體會到它在代碼依賴方面展示出的規範性和嚴謹性。
sync.Map
廣大 gopher 們又迎來了一個提供併發安全性的高級數據結構——sync.Map。這個數據結構提供了一些經常使用的鍵、值存取操做方法,並保證了這些操做的原子性。同時,它也保證了存取的性能——算法複雜度依舊是 O(1) 的。相信不少人已經期盼了好久。不過請注意,它是 Go 語言標準庫中的一員,而不是語言層面的東西。也正由於這一點,Go 對它的鍵類型和值類型並沒有程序編譯期的類型檢查。咱們只能在程序運行期自行保證鍵、值類型的正確。
若是你一直關注 Go 語言,可能已經猜到它就是以前一直蟄伏在 golang.org/x/sync/syncmap 包中的那個 struct。如今它被歸入了標準庫,並將會得到更多的底層優化的可能。
在 sync.Map 問世以前,咱們若是須要併發安全的字典結構,那麼就須要自行搭建。這其實也不是麻煩事,使用 sync.Mutex(互斥鎖)或 sync.RWMutex(讀寫鎖)在再加上原生的數據類型 map 就能夠輕鬆辦到。Github 網站上就有不少庫提供了相似的數據結構。我在《Go 併發編程實戰》第 2 版中也提供了一個實現較完整的併發安全字典。它的性能比同類的第三方數據結構還要好一些。由於它在很大程度上有效地避免了對鎖的依賴。
你們應該都知道,使用鎖就意味着要把一些併發的操做強制串行化。這對程序的性能是有很大的負面影響的,尤爲是在有多個 CPU 核心的狀況下。所以咱們常說,能用原子操做就不要用鎖。惋惜前者只對一些基本數據結構提供支持。
無論是哪種操做,它們在多個 CPU 核心面前都是相對低效的。由於只要是對共享狀態(好比多個線程均可見的同一個變量)的存取就會涉及到狀態的同步。在 CPU 層面,這種同步就是 cache 級別的,也可稱之爲 cache contention。你必定據說過 CPU 的 L1 cache 和 L2 cache。舉個例子,若是在不一樣 CPU 中運行的線程同時在操做同一個鎖(更確切地說是鎖中的計數變量,具體可參看 sync.Mutex 的源碼),那麼它們會爭相聲明存在於本身的 cache 中的那個變量的值是惟一有效的(或者說是最新的)。一旦有一個線程聲明成功,那麼運行於其餘 CPU 核心中的線程再想存取相同的變量就必須先從前者那裏作同步。這個同步的耗時在 CPU 層面是很可觀的。
那麼,sync.Map 幫咱們解決上述問題了嗎?很遺憾,答案是沒有徹底解決。實際上,這根本就不是在應用層面(甚至操做系統層面)能夠徹底解決的問題。咱們只能通過一輪又一輪的優化得到更高的性能,或者說逐漸逼近最高性能。
sync.Map 在內部使用了大量的原子操做存取鍵和值, 並利用兩個 map 做爲存儲介質。
其中一個 map(字段 read)可被視做一個快照。這個快照也可被稱爲只讀字典。它保存了在前一個操做週期結束時 sync.Map 值中包含的全部鍵值對。雖然其中的鍵所對應的值均可以被更改,可是鍵毫不會有增減。所以,這裏的「只讀」是對其中的鍵的集合而言的。如此一來,對只讀字典的操做無需使用鎖。實際上,在 sync.Map 的增、刪、改、查方法中會首先嚐試操做只讀字典。
另外一個 map(字段 dirty)中存有最新的鍵值對的集合。它也可被稱爲髒字典。新的鍵值對會首先被存儲到該字典中。sync.Map 中全部的增、刪、改、查方法在操做髒字典時都須要在鎖(字段 mu)的保護下進行。
在達到當前操做週期的邊界的時候,sync.Map 會把髒字典提高爲只讀字典,並把存儲髒字典的字段設置爲 nil。當再有新的鍵值對存入時,sync.Map 會對髒字典進行從新初始化,並把只讀字典(前一個操做週期中的髒字典)中的全部鍵值對反灌入髒字典,最後把新的鍵值對也加入其中。在新的操做週期中,(新的)只讀字典和(新的)髒字典依然分別表明着鍵值對集合的快照和最新版本。如此一來,經過兩個字典之間的週期性同步,sync.Map 就實現了鍵值對操做的「快路徑」和「慢路徑」。「快路徑」就意味着無鎖化操做,而「慢路徑」僅在「快路徑」不通時纔會被考慮。
順便說一句,sync.Map 在判斷當前操做週期的邊界達到與否時依據的是一個記錄着「在只讀字典中未找到被查詢鍵」這種狀況的發生次數的計數值(字段 misses)。一旦這個計數值等於髒字典的長度,就意味着一個操做週期的結束。顯然,操做週期會隨着髒字典的增大而變長。
圖 4 從鍵值對的流轉方式看 sync.Map
上圖從另一個角度展示了 sync.Map 存取鍵值對的方式。
整體上講,sync.Map 的內部實現就是如此。若是你想探究細節能夠查看 Go 語言標準庫代碼包 sync 中的 map.go 文件。
經過對 sync.Map 的實現的理解,咱們就能夠分析出它的優點和劣勢。顯然,sync.Map 更適合於鍵的集合相對固定的場景。這時只有一些必要的原子操做會對性能有輕微的影響。更具體地講,若是全部的鍵值對都在初始化 sync.Map 值時加入,以後僅有鍵值對的讀取和更新而沒有添加,那麼 sync.Map 就會發揮出很高的性能。固然,若是在多個線程中同時對同一個鍵的值進行更新,那麼還會存在由原子操做引起的競爭。這也會涉及到 cache contention。
另外一方面,若是在使用過程當中有很是多的新鍵存入的話,那麼在極端狀況下它的性能極可能會回退至 map + sync.Mutex/sync.RWMutex 的水平,甚至還會不如後者,別忘了其中還有不少原子操做。另外,請注意,sync.Map 所佔用的空間比 map 要多。至於多多少,還要看實際的鍵值對獲取狀況。
以上就是我對 sync.Map 的簡單剖析。但願可供你們在選擇字典的併發安全策略時參考。
其餘值得注意的改進 並行編譯
Go 1.9 默認會並行地編譯你的代碼。不過前提是你的機器上多個 CPU 核心。其實在這以前並行編譯也是存在的,只不過那時的粒度是代碼包。也就是說,不一樣的代碼包可被並行地編譯。可是 Go 1.9 的並行編譯粒度是函數級別的。這顯然可使編譯更加迅捷。
關於 vendor 目錄的處理
以前咱們執行 go build ./... 命令的時候,當前目錄內的 vendor 目錄也會被編譯。可是如今必須輸入 go build ./vendor/... 才能夠。對於其餘的 Go 標準命令也是如此。我就爲此煩惱過,因此很高興看到這樣的變化。請想象一下,當你在執行 go test ./... 的時候,Go 也會測試你依賴的那一坨代碼包。在不少時候這根本就沒有必要,白白浪費時間。
關於 GC
咱們都知道,Go 的 GC 是併發的。但這隻限於自動 GC。當咱們手動觸發 GC 時,Go 運行時系統依然會「Stop the world」(或者說中止內部調度)。值得慶幸的是,Go 1.9 爲咱們帶來了對手動 GC 的併發支持!這涉及到了 runtime.GC、debug.SetGCPercent 和 debug.FreeOSMemory 函數。不過要注意,調用這些函數的 goroutine 是會被阻塞的,直到 GC 完成。
除此以外,GC 的性能又獲得了進一步提高,尤爲是在擁有龐大(大於 50GB)的堆內存的狀況下。
單調的時間度量
在 Go 1.9 以前,標準庫代碼包 time 中的一些函數是經過讀取計算機的系統時鐘來實現功能的。這樣作的好處是總與系統保持一致,而壞處是一旦系統時鐘被篡改,它們也會無腦地跟着變動。這可能會讓使用它們的 Go 程序產生錯誤,尤爲是咱們在依賴時間作一些任務的時候。請想象一下,千年蟲或者閏秒調整那樣的問題再次出現時會怎樣。所以,Go 必須優雅地應對系統時鐘重置。這在 Go 1.9 中獲得瞭解決。
如今,包括 runtime、time、context 和 net 在內的代碼包都使用了單調的時間度量。在必要時,系統時鐘重置致使的時間度量錯誤會被自動修正。咱們不會再爲此困擾了。
除了上述的這些可喜的改進以後,Go 標準庫也被大範圍地更新了。好比,新的用於處理位操做的 math/bits 包。又好比,testing 包對幫助函數更加友好。還好比,runtime/pprof 包中的諸多改進。等等等等。
在開始撰寫本文以前,Go 1.9.2 早已發佈。在個人 Go 程序研發團隊中有個規定:一般狀況下,當小版本(好比 1.9)的第二個維護版本出來以後,咱們就開始更新各類測試環境甚至生產環境上 Go 的版本。固然,咱們的評估和試用在小版本的第一個版本發佈時就開始了。這既能夠避免因過於激進而踩坑,又能夠儘早享用 Go 版本升級的紅利。在這裏供你們參考。
展 望Go 1.10+
先說說離咱們最近的 Go 1.10。該版本的 beta 版已經發布。它又帶來了衆多改進,好比:更寬鬆的語法、更智能的標準命令、更完善的 Go 彙編支持、無限的最大 P 數量(GOMAXPROCS)設置,以及一如既往的性能提高和標準庫改進。
注意,有兩個標準庫代碼包的變動可能會致使你代碼的必要修改。一個是 bytes 包,涉及到 Fields、FieldsFunc、Split 和 SplitAfter 函數。另外一個是 net/url 包,主要涉及到 ResolveReference 函數。後者不會再想固然地去掉 URL 路徑中的「多餘」斜槓。這樣能夠避免 http.Client 在某些狀況下的重定向錯誤。同時也是爲了符合 RFC 3986 協議。
至於以上變動的細節,請你們參看 Go 1.10 的 release notes。
在 Go 即將滿 10 歲之際,在 Gophercon 2017 大會上,Go 團隊終於把 Go 2 的事情鄭重地擺上了桌面。Go 語言的目標一直是幫助開發者們高效地完成現代軟件的開發和部署。Go 2 的目標仍然如此。但更重要的是,Go 2 會修正 Go 1 中不利於實現前述目標的一些方面。據我我的推測,Go 2 的升級方式會介於 Java 和 Python 之間,同時在廣大開發者可接受的範圍內會更傾向於 Python。你們都知道,Java 的大版本升級過於保守,而 Python 的大版本升級則過於激進。
Go 2 可能會出現很大的變革,但就像 Java、Python 這些「白鬍子」語言同樣,大版本的升級必然會存在不少權衡和妥協,也必然會包含與 Go 1.x 的不兼容。到目前爲止,Go 團隊尚未公佈任何細節。改革錯誤處理方式?增長不可變值?自定義泛型?一切都沒有定數。但不論怎樣,Go 團隊會想盡一切辦法讓廣大開發者平滑過渡到 Go 2 之上。讓咱們翹首期盼吧!
社區,永遠的社區
國內使用 Go 語言的人依然在不斷增加。其增加程度已經讓一些在線教育公司敏銳地嗅到了機會。他們已經把發展 Go 語言相關課程做爲了 2018 年的重點之一。
2017 年我對國內社區的最大感觸就是地方性的 Go 語言組織愈來愈多了。許多省市內的 gopher 們都自行組織起來,一塊兒增強交流、共同增進技能。就拿 12 月 16 日在深圳舉辦的 meetup 來講,當場人數也超過了 100。現場的技術氛圍也很濃郁。你們都在積極地互通有無。可喜可賀!如今各地方的中小型 Go 語言技術聚會基本上均可以達到這個規模了。今年我在北京只組織了一場活動,有些遺憾。我在這裏也反省一下。
我去年呼籲各個使用 Go 語言的公司在 Github 上 Go 語言的 wiki(https://github.com/golang/go/wiki/GoUsers)中加入本身公司的主頁連接。那時,China 那一欄下只有一家公司。現在增長到了 5 家。但是這仍然比我知道的公司少太多了。所以我在今年再呼籲一下。你們多向官方以及國際發聲吧!這對咱們是有好處的。
另外,我好久以前在 Github 上創建了一個國內卓越 Go 項目列表(https://github.com/GoHackers/awesome-go-China),也但願你們能把我的或者公司開源的項目加入其中。如此一來,咱們就能夠在進行 Go 框架和工具選型的時候有一個比較集中、方便的參考之地。同時也能夠增進你們的交流。咱們能夠在 Github 上更有的放矢地爲優秀項目貢獻代碼。另外一方面,這也是吸引代碼貢獻者的另外一個渠道。
從人才市場的方面看,國內招聘 Go 工程師的公司也愈來愈多了。就算在年末,各大 Go 語言微信羣和 QQ 羣裏也能看到不少招聘啓事。看來明年又是 Go 工程師們大展拳腳的一年。
我我的認爲 Go 語言在當前以及可見的將來會更多的應用在以下幾個熱門領域中。首當其衝的固然是雲計算。Go 不只擅長 Web 系統、API 服務等應用層軟件的開發,也能夠用來開發中間件甚至基礎設施。雲計算中幾乎全部的軟件均可以用 Go 語言來開發,並且頗有優點。實際上,在這個領域,能夠說 Go 語言已經成爲你們的首選語言了。這部分得益於 docker、kubernetes 等項目的持續火爆。其次是區塊鏈。如今國內作區塊鏈的公司有不少。這些公司也大都以 Go 語言爲主力。雖然比特幣(或者說加密數字貨幣)如今在國內的狀態不容樂觀。可是做爲其核心技術的區塊鏈卻依然受到熱捧。據我所知,不少 Go 工程師投身其中了,甚至有些國外的區塊鏈創業公司已想在國內開設辦公室了。
順便說一句,對區塊鏈技術有興趣的 gopher 能夠去研究下 hyperledger(https://cn.hyperledger.org/)這個項目。再次是數據科學和機器學習這兩個領域。不少人會說「這都是 Python 的底盤啊,並且幾乎不可動搖」。可是我要說的是,其實在這兩個領域中 Python 也不是一家獨大的,像 R、Julia、MATLAB、C++ 等語言都有本身的一席之地。對標 numpy,最近持續升溫的 Gonum(https://github.com/gonum/gonum)很不錯。另外,Tensorflow 框架也早早放出了 Go 語言版本的 API。固然我不是鼓吹你們在這些領域中使用 Go 語言。各個語言在不一樣領域都有各自的優點,重點是怎樣提升生產力。可是我認爲 Go 語言會在這些領域持續***並擴大優點。各位能夠重點關注一下。
在這個面臨着技術和商業大變革的時代,Go 語言是一項很好的技術資本,起碼你應該讓它做爲你技術工具箱中的重要一員。若是你主攻後端技術但還不會 Go 語言,我建議你花幾個小時入門一下,而後再花一些時間體會和理解它的編程哲學。我相信這對你的技術成長是頗有好處的。
最後,若是你想融入一個國內的 Go 語言技術社區,不妨加入我發起的組織——GoHackers。它的前身是 Go 語言北京用戶組(微信公衆號:golang-beijing)。後者已經有 3 年左右的歷史了。GoHackers 的微信公衆號是「gohackers」,在開發者頭條中的團隊號是「GoHackers」。前者主要用於發佈 Go 語言動向、國內活動預告以及相關招聘啓事,後者主要用來分享優秀的國內外 Go 技術文章。