原文連接: https://github.com/sxs2473/go...
本文使用 Creative Commons Attribution-ShareAlike 4.0 International 協議進行受權許可。
本節包含一些優化 Go 代碼的技巧。git
確保你的 APIs 不會給調用方增長垃圾。github
考慮這兩個 Read 方法golang
func (r *Reader) Read() ([]byte, error) func (r *Reader) Read(buf []byte) (int, error)
第一個 Read 方法不帶參數,並將一些數據做爲[]byte
返回。 第二個採用[]byte
緩衝區並返回讀取的字節數。windows
第一個 Read 方法老是會分配一個緩衝區,這會給 GC 帶來壓力。 第二個填充傳入的緩衝區。緩存
Go 語言中 string
是不可改變的,而 []byte
是可變的。服務器
大多數程序喜歡使用 string
,而大多數 IO 操做更喜歡使用 []byte
。網絡
儘量避免 []byte
到 string
的轉換,對於一個值來講,最好選定一種表示方式,要麼是[]byte
,要麼是string
。 一般狀況下,若是你從網絡或磁盤讀取數據,將使用[]byte
表示。閉包
bytes
包也有一些和 strings
包相同的操做函數—— Split
, Compare
, HasPrefix
, Trim
等。併發
實際上, strings
使用和 bytes
包相同的彙編原語。app
使用 string
做爲 map 的 key 是很常見的,但有時你拿到的是一個 []byte
。
編譯器爲這種狀況實現特定的優化:
var m map[string]string v, ok := m[string(bytes)]
如上面這樣寫,編譯器會避免將字節切片轉換爲字符串到 map 中查找,這是很是特定的細節,若是你像下面這樣寫,這個優化就會失效:
key := string(bytes) val, ok := m[key]
Go 的字符串是不可變的。鏈接兩個字符串就會生成第三個字符串。下面哪一種寫法是最快的呢?
s := request.ID s += " " + client.Addr().String() s += " " + time.Now().String() r = s
var b bytes.Buffer fmt.Fprintf(&b, "%s %v %v", request.ID, client.Addr(), time.Now()) r = b.String()
r = fmt.Sprintf("%s %v %v", request.ID, client.Addr(), time.Now())
b := make([]byte, 0, 40) b = append(b, request.ID...) b = append(b, ' ') b = append(b, client.Addr().String()...) b = append(b, ' ') b = time.Now().AppendFormat(b, "2006-01-02 15:04:05.999999999 -0700 MST") r = string(b)
% go test -bench=. ./examples/concat/
個人測試結果:
goos: darwin goarch: amd64 pkg: test/benchmark BenchmarkConcatenate-8 2000000 873 ns/op 272 B/op 10 allocs/op BenchmarkFprintf-8 1000000 1509 ns/op 496 B/op 13 allocs/op BenchmarkSprintf-8 1000000 1316 ns/op 304 B/op 11 allocs/op BenchmarkStrconv-8 2000000 620 ns/op 165 B/op 5 allocs/op PASS
goos: darwin goarch: amd64 pkg: test/benchmark BenchmarkConcatenate-8 1000000 1027 ns/op 271 B/op 10 allocs/op BenchmarkFprintf-8 1000000 1707 ns/op 496 B/op 12 allocs/op BenchmarkSprintf-8 1000000 1412 ns/op 304 B/op 11 allocs/op BenchmarkStrconv-8 2000000 707 ns/op 165 B/op 5 allocs/op PASS
全部的基準測試在1.11版本下都變慢了?
Append 操做雖然方便,可是有代價。
切片的增加在元素到達 1024 個以前一直是兩倍左右地變化,在到達 1024 個以後以後大約是 25% 地增加。在咱們 append 以後的容量是多少呢?
func main() { b := make([]int, 1024) fmt.Println("len:", len(b), "cap:", cap(b)) b = append(b, 99) fmt.Println("len:", len(b), "cap:", cap(b)) } output: len: 1024 cap: 1024 len: 1025 cap: 1280
若是你使用 append,你可能會複製大量數據併產生大量垃圾。
若是事先知道片的長度,最好預先分配大小以免複製,並確保目標的大小徹底正確。
Before:
var s []string for _, v := range fn() { s = append(s, v) } return s
After:
vals := fn() s := make([]string, len(vals)) for i, v := range vals { s[i] = v } return s
使 Go 很是適合現代硬件的關鍵特性是 goroutines。goroutine 很容易使用,成本也很低,你能夠認爲它們幾乎是沒有成本的。
Go 運行時是爲運行數以萬計的 goroutines 所設計的,即便有上十萬也在乎料之中。
可是,每一個 goroutine 確實消耗了 goroutine 棧的最小內存量,目前至少爲 2k。
2048 * 1,000,000 goroutines == 2GB 內存,什麼都不幹的狀況下。
這也許算多,也許不算多,同時取決於機器上其餘耗費內存的應用。
雖然 goroutine 的啓動和運行成本都很低,但它們的內存佔用是有限的;你不可能建立無限數量的 goroutine。
每次在程序中使用go
關鍵字啓動 goroutine 時,你都必須知道這個 goroutine 將如何退出,以及什麼時候退出。
若是你不知道,那這就是潛在的內存泄漏。
在你的設計中,一些 goroutine 可能會一直運行到程序退出。這樣的 goroutine 不該該太多
永遠不要在不知道該何時中止它的狀況下啓動一個 goroutine
實現此目的的一個好方法是利用如 run.Group, workgroup.Group 這類的東西。
Peter Bourgon has a great presentation on the design behing run.Group from GopherCon EU
Go 運行時使用高效的操做系統輪詢機制(kqueue,epoll,windows IOCP等)處理網絡IO。 許多等待的 goroutine 將由一個操做系統線程提供服務。
可是,對於本地文件IO(channel 除外),Go 不實現任何 IO 輪詢。每個*os.File
在運行時都消耗一個操做系統線程。
大量使用本地文件IO會致使程序產生數百或數千個線程;這可能會超過操做系統的最大值限制。
您的磁盤子系統可能處理不數百或數千個併發IO請求。
若是你寫的是服務端程序,那麼其主要工做是複用網絡鏈接客戶端和存儲在應用程序中的數據。
大多數服務端程序都是接受請求,進行一些處理,而後返回結果。這聽起來很簡單,但有的時候,這樣作會讓客戶端在服務器上消耗大量(可能無限制)的資源。下面有一些注意事項:
若是內存都不算快,那麼相對來講,IO操做就太慢了,你應該不惜一切代價避免這樣作。 最重要的是避免在請求的上下文中執行IO——不要讓用戶等待磁盤子系統寫入磁盤,甚至連讀取都不要作。
儘量避免將數據讀入[]byte
並傳遞使用它。
根據請求的不一樣,你最終可能會將兆字節(或更多)的數據讀入內存。這會給GC帶來巨大的壓力,而且會增長應用程序的平均延遲。
做爲替代,最好使用io.Reader
和io.Writer
構建數據處理流,以限制每一個請求使用的內存量。
若是你使用了大量的io.Copy
,那麼爲了提升效率,請考慮實現io.ReaderFrom
/ io.WriterTo
。 這些接口效率更高,並避免將內存複製到臨時緩衝區。
永遠不要在不知道須要多長時間才能完成的狀況下執行 IO 操做。
你要在使用SetDeadline
,SetReadDeadline
,SetWriteDeadline
進行的每一個網絡請求上設置超時。
您要限制所使用的阻塞IO的數量。 使用 goroutine 池或帶緩衝的 channel 做爲信號量。
var semaphore = make(chan struct{}, 10) func processRequest(work *Work) { semaphore <- struct{}{} // 持有信號量 // 執行請求 <-semaphore // 釋放信號量 }
defer
是有成本的,由於它必須爲其執行參數構造一個閉包去執行。
defer mu.Unlock()
至關於
defer func() { mu.Unlock() }()
若是你用它乾的事情不多,defer
的成本就會顯得比較高。一個經典的例子是使用defer
對 struct 或 map 進行mutex unlock
操做。 你能夠在這些狀況下避免使用defer
固然,這是爲了提升性能而犧牲可讀性和維護性的狀況。
終結器是一種將行爲附加到即將被垃圾收集的對象的技術。
所以,終結器是非肯定性的。
要運行 Finalizers,要保證任何東西都不會訪問該對象。 若是你不當心在 map 中保留了對象的引用,則 Finalizers 沒法執行。
Finalizers 做爲 gc 的一部分運行,這意味着它們在運行時是不可預測的,而且它會與 減小 gc 時間 的目標相悖。
當你有一個很是大的堆塊,而且已經優化過你的程序使之減小生成垃圾,Finalizers 可能纔會很快結束。
提示 :參考 SetFinalizer
cgo 容許 Go 程序調用 C 語言庫。
C 代碼和 Go 代碼存在於兩個不一樣的世界中,cgo 用來轉換它們。
這種轉換不是沒有代價的,主要取決於它在代碼中的位置,有時成本可能很高。
cgo 調用相似於阻塞IO,它們在操做期間消耗一個系統線程。
不要在一個 tight loop 中調用 C 代碼。
cgo 的開銷很高。
爲了得到最佳性能,我建議你在應用中避免使用cgo。
Go 的舊版本永遠不會變得更好。他們永遠不會獲得錯誤修復或優化。
Go 的舊版本不會有任何更新。 不要使用它們。 使用最新版本,你將得到最佳性能。