這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
Dominik Honnef(之前搞錯成 Russ Cox 了)在 What’s happening in Go tip (2013-08-23) 中介紹了一些關於 Go 語言的一些變化。這些變化包含了文法、效能、潛在風險和工具鏈。並且,這些新的東西可能會隨著 Go 1.2 版本一同發布。為了方便中文讀者,翻譯在此。
————翻譯分隔線————
Go tip(2013-08-23)帶來的變化
上周我發布了關於 Go tip 的變化的系列文章的第一篇。得到了大量的肯定,因此這是第二篇。感謝你的支援,並且希望你能像喜歡第一篇文章一樣喜歡本文。
有哪些變化
這次,將對以下內容進行探討:
- 關於切片的新文法
- 效能改進
- 快速的,常量時間的 P-256 橢圓曲線
- godoc 到哪去了?
關於切片的新文法
相關 CL:CL 10743046,CL 12931044
首先讓我們來看看附加在 Go 1.2 中的、可能是最具爭議的變化:允許設定 slice 的容量的新切片文法。不過在討論為什麼它存在爭議之前,先來瞭解一下它究竟是什麼。
所有人應當都已經熟悉了切片操作 mySlice[i:j],從 i 到 j 的範圍建立一個新的 slice。mySlice 和新的 slice 將共用底層的數組,並且對其中一個 slice 操作會影響另一個。這是預期的眾所周知的行為。
但一些人可能並未意識到,這兩個 slice 也會共用容量。一個 slice 有長度和容量;當前可以看到的元素的數量和可以訪問到的元素的數量。在使用 append()¹ 時,容量有著重要的作用:當對一個 slice 進行附加時,首先會檢查底層數組是否留有足夠的空間(容量)。如果有,則建立一個有著更大長度的新 slice 指向相同的底層數組,相同的記憶體空間。
¹: 如果容量允許,也可以手工重建 slice 並傳遞長度。也就是 append() 初步完成的事情。
考慮下面的程式碼片段:
orig := make([]int, 10, 20)subSlice := orig[5:10]fmt.Printf("len(orig) = %d, cap(orig) = %d, len(subSlice) = %d, cap(subSlice) = %d\n",len(orig), cap(orig), len(subSlice), cap(subSlice))// 輸出:len(orig) = 10, cap(orig) = 20, len(subSlice) = 5, cap(subSlice) = 15orig = append(orig, 1)fmt.Println(orig)// 輸出:[0 0 0 0 0 0 0 0 0 0 1]subSlice = append(subSlice, 2)fmt.Println(orig)// 輸出:[0 0 0 0 0 0 0 0 0 0 2]
這證明了 subSlice,即使切片為 orig 的一個視窗,仍然訪問相同的記憶體,由於切片不能收縮,所以至少在前 5 個元素是這樣。也同樣證明在使用 append 會導致“詭異”的結果,導致原先的 slice 中的元素被覆蓋。
那麼,為什麼這在真實的代碼中是一個問題呢?設想當你基於 []byte 編寫了一個自己的記憶體 Clerk——當有處理大量的可能降低 GC 效能的小記憶體配置時,這種方式很常見。當使用者請求了 N 位元組的記憶體,你返回了一個從 []byte 上的某處切片出來的 slice,長度為 N。然後使用者做了一些傻事情:他對其進行 append 操作。跟之前我們看到的一樣,這個 append 將會超過 slice 的長度把記憶體“泄漏”出去,同時也超出記憶體 Clerk預期的那樣。你改寫了其他的記憶體!
一個安全的選擇是允許實現自訂的記憶體 Clerk,不過通過限制返回的 slice 的容量來防止記憶體被侵蝕,這樣 append 就不會在不應該的地方修改記憶體。而這個安全的選擇就是新的切片文法:mySlice[i:j:k] —— 前面的兩個元素跟以前一樣,從 i 到 j 進行切片。第三個元素表示容量,這裡的容量為 k – i 的結果。
簡單來說,k – i 作為容量也就意味著 k – 1 是可以被新的 slice 訪問到的映射到底層數組絕對序號,跟序號 j – 1 是長度一樣。讓 k 等於 j,就建立了一個不能向後訪問超過其長度的 slice。現在就可以編寫一個返回 N 位元組的記憶體,並且不允許向後訪問的分配器了。
在介紹中,我說這個補充是存在爭議的。你可能已經猜到了原因:如果你繼續思考“為什麼我需要這個”,你就會猜到。這個特性極少會用到。只有極少的用例會需要它,標準庫中也沒有多少用到的例子。但這並不意味著這個特性不應當存在,雖然它解決了一個有效問題,不過也被質疑切片文法不應當被擴充。像 setcap() 這樣的函數被建議用作避免向 Go 添加新的文法,不過最終 Go 團隊決定採納新的切片文法。如果你感興趣,這裡有前前後後相關的討論。當然,這會包含在 Go 1.2 中。
還有設計文檔,描述了原始的動機和思路。
效能改進和記憶體回收
相關 CL:CL 11573043,CL 9129044,CL 12894043,CL 8179043,CL 9492044,CL 9432046,CL 8819049,CL 9459044,CL 12603049,CL 12708046,CL 10079043,CL 8670044,CL 12680046,CL 12694048,CL 11874043,CL 12072045,CL 9915043,CL 12662043,CL 9462044
將會在 Go 1.2 中看到許多的效能改進。這些改進主要包括兩類:更好的代碼和編譯器帶來的直接的效能提升,和減少垃圾帶來的記憶體 Clerk和記憶體回收行程的工作的減少。
Brad Fitzpatrick 在探索高效能 HTTP 的時候,已經在 net/http 包和相關的常用包中進行了許多的工作。他的大多數修改都落在了“減少垃圾”分類中。由於大多數修改都十分簡單,我僅列出 CL 編號以便你檢出代碼。全部這些在描述部分都包括了效能測試:CL 9129044(更快的 JSON 編碼),CL 12894043(更快的 ZIP 壓縮),CL 8179043,CL 9492044 和 CL 9432046(若干 HTTP 的改進)。
不管怎樣,有些 CL 確實很有趣。一組 CL 是 CL 8819049,CL 9459044 和 CL 12603049 —— 前面兩個為 bufio 添加了緩衝池和緩衝重用,而最後一個移除了前面所說的緩衝池和緩衝重用,將其用一個簡單的 Reset 方法替代了。這樣做的原因是,這些緩衝區可以被使用者控制,並且可能繞過 API 來損壞其他使用者的資料,產生如“use after free”這樣的錯誤,而這類錯誤由於 Go 明確的避免提供手工的方式來釋放記憶體,會導致難以調試。並且,它為不必要的複雜代碼引起額外的效能開銷埋下了伏筆。
新添加的 Reset 方法用來達到相似的效能改進,強制使用者啟用重用 buffer,而不是依賴於包完成這個。net/http 就是眾多使用者中的一個,使用 Reset 的實現在 CL 12708046。務必注意,效能測試是與池化的 bufio 進行對比,而不是 Go 1.1。
對於原始的討論,參閱 Russ Cox 提交的 issue 6086。
Brad 名下的最後一個修改是 CL 10079043,彙總了多個進行中的 DNS 查詢。或者換種說法:即使同一個 DNS 查詢被執行了上百次,且都未結束的時候,其實只向伺服器發送了一個真正的查詢請求。
不過不僅 Brad 做了工作,其他人也貢獻了大量的改進。
必須提到的一個是 CL 8670044,它為 Windows 實現了整合的網路池,使得在這個平台上的網路操作效能提升了 30% —— 在 Linux 上 Go 1.1 已經實現了的奢侈品。
另一個必須提及的應當是 CL 11573043,它極大的提升了 sync.Cond 的速度 —— 並且完全剔除了處理過程中的記憶體配置。
CL 12680046 和 CL 12694048 為 encoding/binary 添加了快速路由,處理整數類型的 slice,使得這一常見類型的處理又快,成本又低廉。
DES 加密得到了五倍的速度提升(不過,悲哀的是,它仍然很慢),感謝 CL 11874043 和 CL 12072045。
bzip2 解壓縮的速度快了 30%(CL 9915043),而 net 包中的 DNS 用戶端產生了少於 350 位元組的更少的垃圾(CL 12662043)。
最後,但是並不是不重要的,io.Copy 現在通過 ReaderFrom 的 WriterTo 來區分優先順序,當類型實現了著兩個函數時,這會帶來極大的可以看到的效能改進(且在 ioutil.Discard 中幾乎沒有操作)。
快速的,常量時間的 P-256 橢圓曲線
相關 CL:CL 10551044
一個讀者向我指出,添加了常量時間的 P-256 實現是一個非常有趣的變化,而這一實現比之前的也快了不少。
當然效能很好,不過真正的要點在“常量時間”:函數對於所有可能的輸入都花費相同的時間,這避免了計時攻擊,一種通過計時資訊進行密碼破解的旁路攻擊方式。
godoc 到哪去了?
使用 Go tip 總是要求你對開發過程更加熟悉,並且隨時準備著損壞、詭異的行為和突然的改變。
然而,也不是每個使用 Go tip 的人都有時間檢查整個開發記錄,特別是在被告知“嘗試 Go tip 看看你的問題是不是能解決”的時候。例如,有一大票人最近在編譯了 Go tip 以後,都很驚訝和迷惑,godoc 不再工作了,不光是沒有二進位檔案了,有的甚至還提示了一個錯誤:
readTemplate: open /Users/rsc/g/go/lib/godoc/codewalk.html: no such file or directory
在兩種情況裡,修複方式都是運行`go get code.google.com/p/go.tools/cmd/godoc` —— godoc 已經被移動到了 go.tools 子版本庫中。
未來
呼~從 Go 1.1 開始就一直在趕開發進度,但是終點還很遙遠。幸運的是當前的 Go tip 的開發已經減速了,不再產生許多有趣的變化。而哪些重要的事情發生變化時,例如 godoc 的變化,我們講在文章裡提及。
有心的讀者可能已經留意到之前的文章承諾了共用連結的支援。不幸的是,為了給更加重要的改變留出空間,也為了讓大家能更好的理解切片文法的變化,在本文中並未提及。
下周有什麼計劃?更多的變化!