Categories
程式開發

解讀Go語言的2019:如果驚喜不再 還有哪些值得關注?


本文是 InfoQ“解讀 2019”年終技術盤點系列文章之一。

因那些科幻電影而讓大家有著無限憧憬的2020年已來!然而,我們卻依然處在人工的智能階段。時下如火如荼的人工智能與真正的智能之間還有著相當長的一段距離。作為緊跟時代步伐的軟件開發者,我們還是應該務實一些,多做一些腳踏實地的事情,尤其是在構建底層的基礎設施方面。而Go語言正是我們做這類事情時所需要的強大工具。

趨勢:排名15,仍處主流之列

TIOBE Index來看,Go語言最近在全球的熱度似乎有所下滑。不過,如果看總體排名的話,截止到2019年的12月,Go語言依然排在第15位,仍處於主流之列。雖然中途存在一些起落,但總體上還是與去年同期持平的。

解讀Go語言的2019:如果驚喜不再 還有哪些值得關注? 1

圖1:TIOBE Index之Go語言(2019年12月)​

解讀Go語言的2019:如果驚喜不再 還有哪些值得關注? 2

圖2:TIOBE Index(2019年12月)​
簡單來講,TIOBE Index在給這些編程語言排序的時候,依據的是它們在幾個主流搜索或媒體平台上的相關展示數。這些平台包括Google、Baidu、Yahoo、CSDN、Bing、Amazon、Hao123等等。所以說,還是比較客觀的。

Google Trends來看,Go語言在全球的趨勢是穩中有升的。去年此時,Go語言熱度還是82,而現在它已經慢慢地升到了90。

解讀Go語言的2019:如果驚喜不再 還有哪些值得關注? 3

圖3: Google Trends之Go語言趨勢(2019年12月)​

如果我們把目光聚焦在國內,那麼就可以看到,北京依然是Go語言使用者的主要聚集地,沒有之一。但不得不說,深圳、杭州、成都、上海、廣州等城市也有很多優秀的Go程序開發者。

解讀Go語言的2019:如果驚喜不再 還有哪些值得關注? 4

圖4: Google Trends之Go語言熱度(2019年12月)​

從我的觀察來看,Go語言已經過了第二波快速推廣期,並且進入了穩定發展期,起碼在國內是這樣。已經在使用Go語言的技術團隊逐漸開始專注於埋頭寫代碼,而剛開始使用Go語言的團隊也沒有進行大肆的宣傳。Go語言已經悄悄地變成了我們的家常便飯。

領域:有擅長也有短板

Go語言目前所擅長的領域仍然在服務端的Web系統、API網關、中間件、緩存系統,以及數​​據庫、容器技術和雲計算等方面。在這些方面,我們有非常多的框架可以選擇。其中最受歡迎的框架和軟件如下:

此外,還有大名鼎鼎的Kubernetes及其生態圈中的那些知名項目。更多的優秀Go語言項目可以參考我在極客時間專欄《Go語言核心36講》中的思維導圖。如果你只是想知道國內的Go程序開發者都發布了哪些開源項目,那麼也可以參考我發起的awesome-go-China項目。

另一方面,對於那些面向企業的管理系統,Go語言更是不在話下。在最近發布的一份2019 年十大企業級編程語言榜單中,Go語言排在第4位。在它前面的只有JavaScript、Java和Python這三門歷史悠久的編程語言。由於Go語言在軟件的開發、構建、測試、部署等方面做得都非常好,所以它對於企業級軟件研發來說也有很強的競爭力。

當然了,Go語言明顯不擅長的領域也有幾個。雖然Go語言已經對一些移動端和嵌入式設備有所支持,但終歸還未成熟。況且,在這些方面至今還沒有出現過殺手級別的應用項目。如果非要找出來一個的話,我覺得只有Gobot項目才夠資格。另外,Go語言在科學計算、數據科學、機器學習等領域的介入依舊非常少。對於作為前沿中的前沿的人工智能,Go語言也少有涉足。我目前只知道有一個用於科學計算的Gonum項目發展得還算可以。不過,不要忘了,人工智能以及將來會與之相伴的物聯網依然需要服務端軟件、需要雲計算。而Go語言早已在這里站穩了腳跟。

最後,我們再來說一說區塊鏈。區塊鏈的名聲源自數字貨幣。而數字貨幣之亂從未停息過。所以,區塊鏈在人們的心中早已不那麼純潔了。恐怕它已經成為了計算機軟件領域的哈姆雷特。幾乎每一個知道區塊鏈的人在談論到它的時候心裡都會描繪著不同的情景。儘管如此,一些有情懷的廠商正在使用區塊鏈技術做著造福大眾的事情,比如:支付寶、輕鬆籌等等。不得不說,Go語言是區塊鏈頂級技術的有力競爭者,而且它也早已成為了區塊鏈領域中必不可少的技術技能。我們在這方面可以選擇的平台或框架已經有不少了,如Go EthereumFabricCosmosEris等。另外,還有一些不錯的基於區塊鏈技術的數據存儲項目,如CovenantSQLSia等。

變化:趨於穩定

在2019年,Go語言的版本已經更新到了1.13。然而,由於它在語言規範方面已經趨於穩定,所以只有一些小幅變化。其中,與我們最貼近的就是它在數值字面量方面的改進。

數值字面量

Go語言的數值字面量現在可以通過一些前綴來表明不同的製式了。前綴 0b 和 0o 分別可以用於表示二進制整數和八進制整數,比如 0b1110 代表整數 14,0o770 代表整數 504,等等。而前綴 0x 是之前就有的,它可以用於表示十六進制整數。不過,它現在還可以表示十六進制的浮點數,比如 0x10p+1 代表 32.000000。這實際上屬於科學記數法。其中的 p+1 是這個浮點數的指數部分。我們可以在 p 的後面追加正負號和代表指數的十進制整數,以表明需要在前一個部分的基礎上再乘以 2 的幾次方。因此,p+1 表示需要再乘以 2 的 1 次方。又由於 0x10 代表整數 16,所以 0x10p+1 表示的就是浮點數 32.000000。同理,0x10p+2 代表浮點數 64.000000,而 0x10p-1 代表浮點數 8.000000,以此類推。不僅如此,以上的這些表示法現在也都可以被用來表示虛數,如 1.e+3i 等。

除此之外,Go語言終於開始支持數字分隔符了!這個改進點雖然小,但卻是一個千呼萬全的優化。它其實是一個語法糖,讓我們可以在一長串的數字之間插入分隔符“_“,以便使人們更加容易地讀出或寫入這些數字。更讚的是,這個分隔符被插入到哪裡都是可以的。比如,對於整數 123456789,我們寫成 123_456_789 或者 1_2345_6789 都是沒有問題的。甚至,我們還可以在上述的各種表示法中運用這個分隔符,如 0x67_89 和 3.14_15,等等。別擔心,雖然這樣的數值字面量都包含了這麼一個明顯的非數字字符,但是卻不會影響到Go語言對它們的解析。 Go語言會在適當的時候忽略掉其中的分隔符。

這些在數值字面量方面的改進可以讓Go語言更好的融合到新領域的軟件開發當中。比如,我們在開發財務軟件和金融系統的時候就很需要這些特性。一方面,這樣的字面量可以大大地降低我們在編碼時出錯的概率。而另一方面,它們還可以明顯地提高程序的可讀性,從而減少維護者的認知成本和工作量。

移位操作

Go語言中的移位操作可以對處在移位運算符左側的數值(或稱操作數)進行二進制移位,移位的具體次數會由處在移位運算符右側的數值(或稱移位計數)決定。下面是一些簡單的例子:

operand1 := -2
count1 := 1
fmt.Printf("%d << %d: %dn", operand1, count1, operand1<> %d: %dn", operand1, count1, operand1>>count1)

輸出內容:

-2 <> 1 // -1

在Go 1.13發布之前,移位計數的類型必須是某個無符號的整數類型。對於Go的語言規範和具體實現來說,這樣做確實可以減少一定的工作量。但是,對於應用程序的開發者而言,這裡卻隱藏著無法忽略的的工作成本。當我們想把一個整數作為移位計數的時候,必須要保證它的類型是無符號的。如果它不是,那麼我們就不得不進行某種手動的類型轉換,否則程序在被編譯時就會出錯。即使這個整數確實是一個正數,也是如此。我相信大多數的程序開發者都會覺得這很麻煩,而且沒有必要。

然而,更重要的還不是這個工作量的問題。如果我們在將要進行移位操作的時候強行地把某個負數轉換為無符號整數類型的值,那麼就很有可能遇到非預期的情況,從而在程序中埋下一個非常不易察覺和定位的BUG。示例如下:

operand2 := -1
count2 := -8
count2u := uint8(count2)
fmt.Printf("%d << %d: %dn", operand2, count2u, operand2<

輸出內容:

-1 << 248: 0

顯然,把 -1 向左移位 248 次很可能並不是我們想要的。

鑑於上述原因,Go語言團隊在Go 1.13中修正了這個問題。移位計數不再被要求必須是無符號整數類型的值了,只要是整數值就可以。我們可以想像一下,當設定的移位計數小於零時,如果Go程序會立即拋出一個運行時恐慌(panic),而不是悄悄地吞沒了BUG並改變了我們的意圖,那麼我們是不是會更開心一些呢?因為程序中的BUG被更早地暴露了出來,而且BUG的定位和修復還都很容易。

順便說一下,針對移位操作的這一改變使得Go語言的一些行為更加一致了。比如,雖然 len 函數一定會把某個正整數作為其結果值,但它的結果類型卻是 int。又比如,我們在創建一個切片的時候可以使用 make 函數。雖然它的第二個參數值必須是某個正整數,但是這個參數的類型卻也是 int。之所以它們沒有通過類型來嚴格地約束被傳遞的值,正是因為那可能會催生出一些額外的類型轉換代碼。我們很難保證對於任何值的類型轉換都一定會產生符合預期的結果。況且,這還很可能會掩蓋掉一些本來應該暴露出來的錯誤。

當然了,這一改變也帶來了一個新的問題,那就是我們需要自行保證移位計數為正數。不過,這並不困難,也是我們應該做的。而且,當移位計數是由一個常量代表的時候,Go語言的編譯器會先對它進行檢查。如果這個常量不是一個正整數,那麼程序是不會通過編譯的。因此,這個新問題所帶來的影響就基本上可以忽略不計了。

字典的打印

我們都知道,基本上所有的編程語言中的常規字典(或者說映射)都屬於無序的容器。也就是說,它們的實例都不會對其包含的鍵值對的先後順序作出任何的保證。這就意味著,雖然我們可以通過某個鍵快速地從字典中獲取對應的值,但不能指望通過迭代快速地得到這個鍵值對。因為我們不知道這個鍵值對會在第幾次迭代時被返回。字典的這一特性是由它的內部結構決定的。 Go語言中的字典也是如此。

這就導致了一個問題。我們很難有效地觀察字典的即時狀態。由於字典迭代的無序性,通過遍歷字典進行觀察就變成了一種很糟糕的方式。尤其是當字典包含了成千上萬個鍵值對的時候,我們想用肉眼去發現前後兩個狀態的異同幾乎是不可能的。然而,我們若要用程序去自動地識別它們的不同,其效率顯然也不會太高。下面舉例說明。

我們現在有一個 map(int)string 類型的字典 map1,並且已經以從小到大的順序放入了一些鍵值對。其中的鍵都是一些正整數,而與之對應的值都是鍵的字符串形式,比如,整數值 1 與字符串值 “1” 會共同組成一個鍵值對。如此一來,下面的代碼就會遍歷這個字典並依據迭代的次序打印出其中的各個鍵值對:​

fmt.Printf("Map: (n  ")
for k, v := range map1 {
    fmt.Printf("%d:%s ", k, v)
}
fmt.Println("n)")

輸出內容:

Map: (
  3:3 4:4 7:7 5:5 6:6 8:8 9:9 0:0 1:1 2:2
)

我們可以看到,打印出來的內容體現的是“亂序”的鍵值對。正因為如此,我們無法在這裡用比較字符串值的方式來比較字典的多個即時狀態。我相信,大多數寫過Go程序測試代碼的gopher們都為此苦惱過。

好消息是,從2019年的2月份開始,我們終於對此擁有了一種有效的手段。在Go 1.12中,官方團隊對 fmt 包中的一系列打印函數進行了優化。這使得它們將會依據鍵的大小去依次地打印字典中的鍵值對。相應代碼如下:

fmt.Printf("Map: %vn", map1)

輸出內容:

Map: map(0:0 1:1 2:2 3:3 4:4 5:5 6:6 7:7 8:8 9:9)

這個結果就不用我多解釋了吧。這些鍵值對是以升序排列的。不過要注意,字典在被迭代的時候依然會以“亂序”的方式吐出一個個鍵值對。所以,前一種打印方式所產生的結果依舊。而有序的鍵值對只會體現在上面這種整體打印的情況下。

讓我們再稍微深入一點。在整體打印的過程中,對於不同類型的鍵,打印函數的比較方式也會有所不同。具體如下:

  • 先來說基本類型的鍵。對於整數值、浮點數值和字符串值,打印函數會利用標準的比較操作進行排序。浮點數中的 NaN 會比其他的浮點數值更小。對於復數值,打印函數會先比較實部再比較虛部。更具體的比較方式同上。而對於布爾值來說,false 肯定是比 true 更小的。

  • 對於指針值和通道值,打印函數會去比較它們在內存中的地址。請注意,這裡所說的指針值特指通過取址操作符獲得的指針值,並不是 uintptr 類型或者 unsafe.Pointer類型的值。對於 uintptr 類型的鍵,打印函數會把它們當作整數值去比較。而對於 unsafe.Pointer 類型的鍵,打印函數根本就無法比較它們。請記住,對一個鍵類型為 unsafe.Pointer 的字典進行整體打印會引發一個運行時恐慌。

  • 對於結構體值和數組值,打印函數會逐一地比較其中的字段值或元素值,直到能夠判斷出誰大誰小為止。

  • 對於接口類型的值,打印函數會先利用反射識別出它們的實際類型,然後再依據上面的規則進行比較。

  • 最後,如果字典的鍵允許為 nil,那麼 nil 一定是最小的。

這些比較規則很簡單,你肯定花費不了多少時間就可以記住它們。不過,即使你懶得去了解這些細則,也不會妨礙你從中受益。示例代碼如下:

// 生成第一个快照。
snapshot1 := fmt.Sprint(map1)
fmt.Printf("Snapshot 1: %sn", snapshot1)
// 修改 map1:增加一些新的键值对。
for i := max + 1; i  Snapshot 1 ? %vn", snapshot2 > snapshot1)

輸出內容:

Snapshot 1: map(0:0 1:1 2:2 3:3 4:4 5:5 6:6 7:7 8:8 9:9)
Snapshot 2: map(0:0 1:1 2:2 3:3 4:4 5:0 6:6 7:7 8:8 9:9 11:11 12:12 13:13)
Snapshot 2 > Snapshot 1 ? false

面對這樣整齊的輸出,我們即使僅憑肉眼也可以輕易地找出兩個快照的不同,不是嗎?更何況,這樣的輸出對於測試或監測程序來說也是非常友好的。

錯誤的包裝和追溯

Go語言中的error值看起來很普通。它們的內部結構一般都非常的簡單,最多也只是會包含一些有助於定位問題的字符串信息罷了。這讓我們可以輕易地對這些error值進行判斷和比較。

不過,這樣的error值也有一個很明顯的缺點,那就是不便追溯。一旦程序出錯了,我們總是想第一時間知道問題到底出在了哪裡。即使error值包含了一些有用的字符串形式的錯誤信息,我們也往往只能憑藉對程序的熟悉程度和以往的程序調試經驗去推測出錯的原因和具體位置。

不只是這樣,當我們得到一個error值的時候,它有可能代表的並不是那個原發的錯誤,而是由程序內部的錯誤處理代碼轉發出來的另一個新生成的error值。如果那段錯誤處理代碼編寫得當的話,這個新生成的error值應該會包含一些描述原發錯誤的信息。但是,它需要包含多少原發錯誤信息,以及它包含的信息是否真正有用,是沒有一個統一的標準的,基本上全憑那段代碼的編寫者說了算。這種方式顯然太鬆散了,對於代碼質量的管理是非常不利的。

也正因為如此,一些Go程序的優秀開發者已經在編寫更好的錯誤處理包了。目前業界公認的比較好用的代碼包有 github.com/pkg/errors 和 gopkg.in/errgo.v2,等等。

由於開發者們對更好的錯誤處理標準的渴望以及普遍存在的呼聲,Go語言團隊在大約一年以前開始考慮對現有的errors包進行改進。終於,在Go 1.13中,新的錯誤處理機制開始被融入到已有的errors包裡了。下面,我們就來一起看一看這個博眾家之所長的新機制是怎樣的。

首先,我們在一個error值中包含另一個error值的做法終於得到了官方的支持。並且,我們現在可以使用一種標準的方式從前者之中拿出後者。例如,我編寫了兩個錯誤類型:

// DetailedError 是一个有错误详情的错误类型。
type DetailedError struct {
    msg string
}


// Error 会返回 error 的信息。
func (de DetailedError) Error() string {
    return de.msg
}


// WrappedError 是一个可包装其他错误的错误类型。
type WrappedError struct {
    msg   string
    inner error
}


// Error 会返回 error 的信息。
func (we WrappedError) Error() string {
    return we.msg
}


// Unwrap 会返回被包装的 error 值。
func (we WrappedError) Unwrap() error {
    return we.inne

我讓 DetailedError 類型擁有 Error() string 方法,是為了讓它實現 error 接口。 Go程序員肯定都知道這一點。然而,錯誤類型 WrappedError 不只有 Error() string 方法,還有 Unwrap() error 方法。後者是為了讓 errors.Unwrap 函數能夠支持 WrappedError 類型的值。

errors.Unwrap 函數就是我在前面提到的那個標準的方式。它可以從一個錯誤值中取出另一個錯誤值。但前提是,前者必須擁有 Unwrap() error 方法,並且該方法一定會返回該錯誤值包含的那個錯誤值。

基於上面的類型聲明,下面的代碼會打印出如我們所願的內容:

err1_1 := errors.New("unsupported operation")
err1_2 := WrappedError{
    msg:   "operation failed",
    inner: err1_1,
}
fmt.Printf("Message(outer error): %vn", err1_2)
fmt.Printf("Message(inner error): %vnn", errors.Unwrap(err1_2))

輸出內容:

Message(outer error): operation failed
Message(inner error): unsupported operation

我不知道你有沒有發現,這使得我們可以生成一條任意長度的錯誤鏈。持有這條錯誤鏈的程序可以通過errors.Unwrap 函數從近端的(或者說最外層的)錯誤值開始依次地獲取到它包含的所有錯誤值,直到取出最遠端的(或者最內層的)那個錯誤值為止。如此一來,我們就可以一層一層地包裝錯誤值以反映出錯誤發生時的上下文狀態。另一方面,拿到這樣的錯誤值的程序也就有機會知道引發錯誤的根本原因是什麼了。

當然了,我們讓錯誤類型擁有 Unwrap() error 方法不只有這一點好處。這樣做也會讓 errors.Is 函數和 errors.As 函數開始支持此類型。

errors.Is 函數的簽名是 Is(err, target error) bool。它的功能是從 err 以及它直接或間接包含的錯誤值中尋找等於 target 的值。它會沿著錯誤鏈由外及內地對每一個錯誤值進行判斷。一旦找到了相等的錯誤值,它就會返回 true。如果在遍歷完整條錯誤鏈之後仍未找到與 target 相等的錯誤值,那麼它就會返回 false。

errors.As 函數的尋找路徑與 errors.Is 函數是一樣的。只不過,它尋找的是在類型上與目標一致的錯誤值。從該函數的簽名 As(err error, target interface{}) bool 我們就可以了解到,參數 target 雖然會代表某個值,但是這個值的類型才是判斷的真正依據。當我們有如下的兩個變量:

err2_1 := DetailedError{
    msg: "unsupported operation",
}
err2_2 := WrappedError{
    msg:   "operation failed",
    inner: err2_1,
}

那麼,調用表達式 errors.Is(err2_2, err2_1) 和 errors.As(err2_2, &DetailedError{}) 返回的值就肯定都會是 true。注意,errors.As 函數的第二個參數值必須是某個錯誤值的指針值,而不能是錯誤值本身。否則將會引發一個運行時恐慌。

然而,就算已經落實了這些改進,Go 1.13中的 errors 代碼包也依然處於一個“改進中”的狀態。這主要是為了做到循序漸進和保證向後兼容。充分落實了新錯誤處理機制的代碼實際上在 golang.org/x/xerrors 包中。這個代碼包在GitHub上也有託管,地址是 https://github.com/golang/xerrors

利用這個代碼包,我們可以很方便地讓現有的打印函數逐層地打印出一條錯誤鏈中的所有錯誤信息。不過,這就需要我們為錯誤類型添加更多的方法了。代碼如下:

// FormattedError 是可暴露内部错误信息的错误类型。
type FormattedError struct {
    msg   string
    inner error
}


// Error 会返回 error 的信息。
func (fe FormattedError) Error() string {
    return fe.msg
}


// Unwrap 会返回被包装的 error 值。
func (fe FormattedError) Unwrap() error {
    return fe.inner
}


// Format 会打印格式化后的错误值。
func (fe FormattedError) Format(f fmt.State, c rune) {
    xerrors.FormatError(fe, f, c)
}


// FormatError 会返回错误链中的下一个错误值。
func (fe FormattedError) FormatError(p xerrors.Printer) (next error) {
    p.Print(fe.Error())
    return fe.Unwrap(

錯誤類型 FormattedError 不但擁有 Error 方法和 Unwrap 方法,還擁有 Format 方法和 FormatError 方法。其中,Format 方法是現有的錯誤處理機制中的一部分,標準的打印函數在打印一個錯誤值的時候會試圖調用該值的 Format 方法以實現打印內容的自定義。

可以看到,FormattedError 類型的 Format 方法中只有一行代碼,即:xerrors.FormatError(fe, f, c)。這行代碼很關鍵。因為 xerrors.FormatError 函數會在適當的時候調用參數 fe 及其代表的錯誤鏈中的所有錯誤值的 FormatError 方法(如果有的話)。

顯而易見, FormatError 方法是 xerrors 包代表的新錯誤處理機制所特有的。它應有的功能是,在打印當前的錯誤值之後返回該值包含的那個錯誤值。這樣的話,只要這些錯誤值都擁有 FormatError 方法,再加上調用了 xerrors.FormatError 函數的 Format 方法,那麼即使是一個普通的打印函數也可以打印出整條錯誤鏈中的所有錯誤信息。請看下面的示例:

err3_1 := DetailedError{
    msg: "unsupported operation",
}
err3_2 := WrappedError{
    msg:   "operation failed",
    inner: err3_1,
}
err3_3 := FormattedError{
    msg:   "operation error",
    inner: err3_2,
}
fmt.Printf("Error: %vn", err3_3)

​最後一條打印語句會導致如下內容的輸出:

Error: operation error: operation failed

請注意,之所以這行輸出內容中沒有“unsupported operation”,是因為 WrappedError 類型並沒有像 FormattedError 類型那樣的 Format 方法和 FormatError 方法。

由於篇幅原因,關於 xerrors 包的更多情況我就不多說了。我們其實完全可以使用 xerrors 包而不用標準庫中的 errors 包來創建和處理錯誤值。這樣就可以享有新錯誤處理機制所帶來的全部好處了。

其他

除了上述幾個比較重要的改進之外,官方團隊在這一年還對Go語言做了很多的更新和優化。其中,值得我們特別注意的有:

  • Modules基本上已經轉正了。環境變量 GO111MODULE 的默認值已是 auto。並且,即使代碼處在 GOPATH/src 目錄下,Modules機制也會奏效。這就意味著,go 命令在任何情況下都會具有模塊感知能力,從而大大地簡化了舊代碼遷移的工作量。

  • GOPROXY可以有多個了。環境變量GOPROXY的值現在可以是由英文逗號分割的多個地址了,如:GOPROXY=https://goproxy.cn,https://goproxy.io,direct 。 Go的命令行工具在從網絡上獲取代碼包的時候會依次嘗試從這些地址下載。

  • TLS 1.3已成為缺省配置。在代碼包 crypto/tls 中,TLS 1.3已經是缺省的配置了。不過,如果你想繼續使用 TLS 1.2,那麼可以讓環境變量 GODEBUG 的值中包含tls13=0 。當然了,我是不建議這麼做的。而且,這種倒退的選擇將在Go 1.14中被丟棄。

  • Binary-only packages 以後將不會再受到支持。 Go 1.13將是支持它的最後一個版本。我們也可以稱這種包為純二進制包。如果你還不知道這是什麼,那麼以後也不用再去了解了。但倘若你正在使用這種包,請盡快作出相應的替換和更改。

  • 代碼運行的性能又有大幅提升。這包括:defer 語句的性能提高了30%;sync.Once 類型的Do方法快了一倍,互斥鎖和讀寫鎖的方法也提速了10%。 Go運行時中的計時器和deadline檢查代碼更快了,這使得維護網絡鏈接的操作更加高效,並且在多CPU的計算機上擁有更好的可擴展性。

至此,我們已經闡述了Go語言本身在2019年最主要的那些變化。希望這些內容能夠對你使用新版本的Go語言有所幫助。對於這部分所展示的所有代碼,我已經發佈到了GitHub上,你可以前往 https://github.com/hyper0x/go2019 進行查看。

展望:Go 2會不負眾望

在我撰寫這篇文章的時候,Go 1.14的開發週期其實早已開始了,甚至其beta版本都已經發布了。從目前公開的資料來看,新版本的改進主要還是集中在工具鏈的易用性和功能性優化、Modules機制(與早先的vendor等機制)的進一步融合、運行時系統的性能優化、測試包的易用性優化、網絡安全協議實現的更替等方面。此外,還有很多的問題修復和小幅改進。

當前,除了主打的服務端之外,Go語言還在向著大前端(主要是WebAssembly)的方向發力。而早先被納入的移動端在2019年倒是沒有什麼大的動作,主要還是對最新版本的iOS和Android提供了支持。我們當然是希望Go語言能在全端都有良好的發展。但這終歸是要分清主次的。 Go語言的著力點仍然以Web服務程序為中心。我想,在廣義的Go語言技術社區仍需進一步開放、官方團隊的時間和精力依然非常有限的情況下,這也算是一個很好的策略了。

按照原計劃,Go 2 應該會以一種比較平滑的方式出現。關於這一點,我們從官方團隊對 errors 代碼包的更新方式上就能夠看得出來。Go 2草案中提到的一些特性在今後的一段時間裡可能會陸續的以標準庫更新或者新擴展庫的形式融入到Go 1當中,就像 golang.org/x/xerrors 包那樣。如此一來,使用Go 1的大部分開發者就都可以提前享受到一些新特性帶來的好處了。不得不說,Go語言在向後兼容方面做得是相當不錯的。

大家肯定也能感覺得出來,Go 1在語言特性和標準庫方面已經相當穩定了。我認為,除了逐漸融入Go 2的新特性,Go 1應該不會再有大的變化了,進一步的優化、改進和完善應該是官方團隊在短期內的主要工作。

就我個人而言,雖然Go程序在性能方面早已甩掉Python好幾條街,也早就超過了Java,但我依然希望它能夠進一步地把性能優勢發揮到極致,尤其是並發程序方面的性能。另外,我也希望Go語言的Modules機制、錯誤處理機制,以及各種監測和調試工具都能夠更上一層樓、越來越好用。

由於Go語言屬於強類型的編譯型編程語言,所以它在語法的自由度和上手的便捷度方面還是稍顯遜色的。當然了,這件事情有利有弊。每門編程語言都會有自己的權衡。對於團隊級別的軟件開發,尤其是中大規模團隊級別的軟件開發來說,嚴謹的語法是非常有益的。不論怎樣,開發者們(包括我)都希望能用更少的代碼完成更多的工作,同時程序還要更易於維護,並且保持優良的性能(要求實在是很多啊)。我希望也相信Go語言會繼續以自己的方式朝著這方面努力和發展。

我斷定,在今後的幾年中,雲計算和大數據仍然會是非常有潛力和有前途的領域。隨著5G的到來,它們也會為人工智能和物聯網提供強有力的支撐。Go語言目前在雲計算領域非常的受歡迎。不過,它在大數據領域至今還沒有嶄露頭角。起碼還沒有一個殺手級別的應用程序出來。我倒是很盼望能有這樣的程序問世,但是這顯然不太容易。因為,大數據的生態系統在很早以前就被Java平台下的技術霸占了。

最後,雖然Go語言肯定是Google公司的Go語言,但是Go語言團隊現在顯然已經更加的開放了。他們在一步一步地擁抱技術社區,聽取社區的意見和建議、參考和採納社區的想法和技術實現。因此,以這樣的態勢,我堅信Go語言的發展會越來越好,同時對普通的開發者也會越來越友好。相對於Go 1的趨於穩定,我相信將在未來發布的Go 2絕對會不負眾望,也會足夠的驚艷。希望到時候會有更多的愛好者來使用這門優秀的編程語言。

往期文章:

解讀 2015 之 Golang 篇:Golang 的全迸發時代

解讀 2016 之 Golang 篇:極速提升,逐步超越
Go 語言的 2017 年終總結
解讀 2018 之 Go 語言篇(上):為什麼 Go 語言越來越熱?
解讀 2018 之 Go 語言篇(下):明年有哪些值得期待?

官方參考:

Go 1.12 Release Notes: https://golang.org/doc/go1.12

Go 1.13 Release Notes: https://golang.org/doc/go1.13

Go 1.14 Release Notes(DRAFT): https://tip.golang.org/doc/go1.14

Go 1.14 Milestone: https://github.com/golang/go/milestone/95

作者介紹:

郝林,國內知名的Go語言技術佈道者,GoHackers技術社群的發起人和組織者。他發布過很多Go語言技術教程,包括開源的《Go命令教程》、極客時間的付費專欄《Go語言核心36講》,以及圖靈原創圖書《Go並發編程實戰》,等等。其中的專欄和圖書都有數万的訂閱者或購買者,而開源教程的star數也有數千。目前,他在繼續研究和實踐各種編程技術和程序設計方法,並在撰寫新的、對初學者更加友好的開源教程《Julia編程基礎》