用golang開發系統軟體的一些細節
作者:張富春(ahfuzhang),轉載時請註明作者和參照連結,謝謝!
(本文的pdf版本)
眾所周知,golang非常適合用於開發後臺應用,但也通常是各種各樣的應用層軟體。
開發系統軟體, 目前的首選還是C++, C, rust等語言。相比應用軟體,系統軟體需要更加穩定,更加高效。其維持自身執行的資源消耗要儘可能小,然後才可以把更多CPU、記憶體等資源用於業務處理上。簡單來說,系統軟體在CPU、記憶體、磁碟、頻寬等計算機資源的使用上要做到平衡且極致。
golang程式碼經過寫法上的優化,是可以達到接近C的效能的。現在早已出現了很多用golang完成的系統軟體,例如很優秀的etcd, VictoriaMetrics等。VictoriaMetrics是Metric處理領域優秀的TSDB儲存系統, 在閱讀其原始碼後,結合其他一些golang程式碼優化的知識,我將golang開發系統軟體的知識總結如下:
個人認為GC掃描物件、及其GC引起的STW,是golang最大的效能殺手。本小節討論優化golang GC的各種技巧。
下面一段神奇的程式碼,能夠減少GC的頻率,從而提升程式效能:
func main(){
ballast := make([]byte, 10*1024*1024*1024)
runtime.KeepAlive(ballast)
// do other things
}
其原理是擴大golang runtime的堆記憶體,使得實際分配的記憶體不容易超過堆記憶體的一定比例,進而減少GC的頻率。GC的頻率低了,STW的次數和時間也就更少,從而程式的效能也提升了。
具體的細節請參考文章:
眾所周知,golang中分配太多物件,會給GC造成很大壓力,從而影響程式效能。
那麼,我在golang runtime的堆以外分配記憶體,就可以繞過GC了。
可以通過mmap系統呼叫來使用堆外記憶體,具體請見:《Go Mmap 檔案記憶體對映簡明教學》
對於堆外記憶體的應用,在此推薦一個非常經典的golang元件:fastcache。具體請看這篇我對fastcache的分析文章:《介紹一個golang庫:fastcache 》。
也需要注意,這裡有個坑:
如果使用mmap去對映一個檔案,則某個虛擬地址沒有對應的實體地址時,作業系統會產生缺頁終端,並轉到核心態執行,把磁碟的內容load到page cache。如果此時磁碟IO高,可能會長時間的阻塞……進一步地,導致了golang排程器的阻塞。
物件太多會導致GC壓力,但又不可能不分配物件。因此物件複用就是減少分配消耗和減少GC的釋放消耗的好辦法。
下面分別通過不同的場景來討論如何複用物件。
假設有很多幾個位元組或者幾十個位元組的,數以萬計的物件。那麼最好不要一個個的new出來,會有兩個壞處:
海量微型物件的影響,請看我曾經遇到過的這個問題:《【筆記】對golang的大量小物件的管理真的是無語了……》
因此,海量微型物件的場景,這樣解決:
當然,也有缺點:不好縮容。
對於大量的小型物件,sync.Pool是個好選擇。
推薦閱讀這篇文章:《Go sync.Pool 保姆級教學》
sync.Pool不如上面的方法節省記憶體,但好處是可以縮容。
有的時候,我們可能需要一些定額數量的物件,並且對這些物件複用。
這時可以使用channel來做記憶體池。需要時從channel取出,用完放回channel。
fasthttp, VictoriaMetrics等元件的作者 valyala可謂是把slice複用這個技巧玩上了天,具體可以看fasthttp主頁上的Tricks with []byte
buffers這部分介紹。
概要的總結起來就是:[]byte這樣的陣列分配後,不要釋放,然後下次使用前,用slice=slice[:0]
來清空,繼續使用其上次分配好的cap指向的空間。
這篇中文的總結也非常不錯:《fasthttp對效能的優化壓榨》
valyala大神還寫了個 bytebufferpool,對[]byte
重用的場景進行了封裝。
對於slice和map而言,在預先可以預估其空間佔用的情況下,通過指定大小來減少容器操作期間引起的空間動態增長。特別是map,不但要拷貝資料,還要做rehash操作。
func xxx(){
slice := make([]byte, 0, 1024) // 有的時候,golangci-lint會提示未指定空間的情況
m := make(map[int64]struct{}, 1000)
}
此技巧源於valyala大神。
假設有一個很小的map需要插入和查詢,那麼把所有key-value順序追加到一個slice中,然後遍歷查詢——其效能損耗可能比分配map帶來的GC消耗還要小。
具體請見這篇:《golang第三方庫fasthttp為什麼要使用slice而不是map來儲存header?》
golang中非常酷的一個語法特點就是沒有堆和棧的區別。編譯器會自動識別哪些物件該放在堆上,哪些物件該放在棧上。
func xxx() *ABigStruct{
a := new(ABigStruct) // 看起來是在堆上的物件
var b ABigStruct // 看起來是棧上的物件
// do something
// not return a // a雖然是物件指標,但僅限於函數內使用,所以編譯器可能把a放在棧上
return &b // b超出了函數的作用域,編譯器會把b放在堆上。
}
valyala大神的經驗:先找出程式的hot path,然後在hot path上做棧逃逸的分析。儘量避免hot path上的堆記憶體分配,就能減輕GC壓力,提升效能。
fasthttp首頁上的介紹:
Fast HTTP package for Go. Tuned for high performance. Zero memory allocations in hot paths. Up to 10x faster than net/http
這篇文章介紹了偵測棧逃逸的方法:
驗證某個函數的變數是否發生逃逸的方法有兩個:
- go run -gcflags "-m -l" (-m列印逃逸分析資訊,-l禁止內聯編譯);例:
➜ testProj go run -gcflags "-m -l" internal/test1/main.go # command-line-arguments internal/test1/main.go:4:2: moved to heap: a internal/test1/main.go:5:11: main make([]*int, 1) does not escape
- go tool compile -S main.go | grep runtime.newobject(組合程式碼中搜runtime.newobject指令,該指令用於生成堆物件),例:
➜ testProj go tool compile -S internal/test1/main.go | grep newobject 0x0028 00040 (internal/test1/main.go:4) CALL runtime.newobject(SB)
——《golang 逃逸分析詳解》
逃逸的場景,這篇文章有詳細的介紹:《go逃逸場景有哪些》
強烈建議在main.go的import中加入下面的程式碼:
import _ "go.uber.org/automaxprocs"
特別是在容器環境執行的程式,要讓程式利用上所有的CPU核。
在k8s的有的版本(具體記不得了),會有一個噁心的問題:容器限制了程式只能使用比如2個核,但是runtime.GOMAXPROCS(0)
程式碼卻獲取到了所有的物理核。這時就導致程序的物理執行緒數接近邏輯CPU的個數,而不是容器限制的核數。從而,大量的CPU時間消耗在物理執行緒切換上。我曾經在騰訊雲上測試過,這種現象發生時,容器內單核效能只有物理機上單核效能的43%。
因此,發現效能問題時,可以通過ls /proc/$(pidof xxx)/tasks | wc
來檢視程序的物理執行緒數,如果這個數量遠遠高於從容器要求的核數,那麼在部署的時候建議加上環境變數來解決:export -p GOMAXPROC=2
協程的排程,本質上就是一個一直在執行的迴圈,不斷的呼叫各個協程函數。然後協程函數在適當的時機儲存上下文,放棄執行,把程式流程再轉回到主迴圈。
這裡有幾個要點:
因此,每個協程函數:在做IO操作的時候一定會切換回主迴圈,編譯器也會在協程函數內編譯進去可以切換上下文的程式碼。新版的golang runtime還存在強制排程的機制,如果某個正在執行的協程不會退出,會強制進行切換。
由於存在協程切換的排程機制,golang是不適合做計算密集型的工作的。例如:音視訊編解碼,壓縮演演算法等。以zstd壓縮庫為例,golang版本的效能不如cgo的版本,即便cgo呼叫存在一定開銷。(我舉的例子比較極端,當需要讓golang的效能達到與C同一個級別時,標題的結論才成立。)
由runtime的排程器原理可知,協程數不是越多越好,過多的協程會佔用很多記憶體,且佔用排程器的資源。
如何剋制的使用協程,請參考我的這篇文章:《VictoriaMetrics中的golang程式碼優化方法》
總結起來就是:
關於優先順序的案例,請參考我寫的這篇文章:《VictoriaMetrics中協程優先順序的處理方式》
當業務環境需要區分重要和不太重要的情況時,要通過一定的機制來協調協程的優先順序。比如存貯系統中,寫入的優先順序高於查詢,當資源受限時,要讓查詢的協程主動讓出排程。
不能讓排程器來均勻排程,不能建立更多的某類協程來獲得爭搶優勢。
要深入理解golang的runtime,推薦閱讀yifhao同學的這篇文章:《萬字長文帶你深入淺出 Golang Runtime》
並行層面的問題是通用性的知識,與語言的特性並無直接的關係。本節列出golang中處理並行的慣用方法,已經對golang的並行處理很熟悉的同學可以跳過本小節。
關於鎖的使用,VictoriaMetrics這個開源元件中有很多經典的案例。也可以移步參考這篇文章的總結:《VictoriaMetrics中的golang程式碼優化方法》(本人)
以生產者-消費者模型為例:如果多個消費者之間可以做到互不關聯的處理業務邏輯,那麼應該儘量避免他們之間產生關聯。其業務處理過程中需要的各個物件,宜各自一份。
擁有JAVA經驗的同學要特別小心這一點:JAVA中,在方法上加上個關鍵字就能實現互斥,但這時非常不好的設計方式。只需要對並行環境下產生衝突的變數加鎖即可,程式碼及其不衝突的變數都是不必要加鎖的。
更進一步,如果存在多個衝突的變數,且在程式中不同的位置發生衝突,那麼可以對特定的一組變數定義一個特定的鎖,而不是使用一把統一的大鎖來進行互斥——儘量使用多個鎖,讓衝突進一步減小。
某些讀寫的場景下,讀是可以並行的,而寫是互斥的。這種場景下,讀寫鎖是比互斥鎖更好的選擇。
var value int64 = 0
atomic.AddInt64(&value, 1) // 原子加
atomic.AddInt64(&value, -1) // 原子減
var n uint64 = 1
atomic.AddUint64(&n, 1)
atomic.AddUint64(&n, ^uint64(0)) // 原子減1,無符號型別,使用反碼來減
newValue := atomic.LoadInt64(&value) // 記憶體屏障,避免亂序執行,並且同步CPU cache和記憶體
atomic.StoreInt64(&value, newValue)
oldValue := atomic.SwapInt64(&value, 0) // 獲取當前值,並清零
原子操作就能搞定的並行場景,就不要再使用鎖。
golang裡面哪來的自旋鎖?
其實我們可以自己寫一個:
var globalValue int64 = 0
func xxx(newValue int64){
oldValue := atomic.LoadInt64(&globalValue) // 相當於使用 memory barrier 指令,避免指令亂序
for !atomic.CompareAndSwapInt64(&globalValue, oldValue, newValue) { // 自旋等待,直到成功
oldValue = atomic.LoadInt64(&globalValue) // 失敗後,說明那一瞬間值被修改了。需要重新獲取最新的值
// 其他數值操作的準備
}
}
以上是無鎖資料結構的經典套路。
有的物件很基礎,可能需要頻繁存取,且有時又會發生參照的切換。比如程式中的全域性設定,很多地方都會參照,有時設定更新後,又會切換為最新的設定。
這種情況下,加鎖的成本太高,不加鎖又會帶來風險。因此,使用sync.Value來儲存全域性設定的資料是個不錯的選擇。
type Configs map[string]string
var globalConfig atomic.Value
func GetConfig() Configs {
v, ok := globalConfig.Load().(Configs)
if ok{
return v
}
return map[string]string{}
}
func SetConfig(cfg Configs){
globalConfig.Store(cfg)
}
並行map設計得很精巧,用起來也很簡單。不過很可惜,sync.Map沒有那麼快,要避免將sync.Map用在程式的關鍵路徑上。
當然,我上述的觀點的區分點是:這是業務程式還是系統程式,如果是系統程式,儘量不要用。我實際使用中發現,sync.Map會導致CPU消耗高,且GC壓力增大。
對某些特定的場景,可以做到很少的鎖,很小的記憶體,比如儲存大量UINT64型別的集合這一點,RoaringBitmap是個非常好的選型。
VictoriaMetrics中有一個RoaringBitmap實現的元件,叫做uint64set。具體介紹請見:《vm中仿照RoaringBitmap的實現:uint64set》(本人)。
channel當然也算一種並行容器,其本質上是無鎖佇列。
需要注意兩點:
因此在大並行下,入隊出隊操作是序列化的,CAS失敗+自旋重試又會帶來cpu使用率升高。
同樣的,channel沒有那麼快。要避免在劇烈競爭的環境下使用channel。
有的運算結果,有一定概率用到,但是又不必每次都計算。這種情況下,使用sync.Once來懶惰初始化是個好辦法:
var once sync.Once
var globalXXX *XXX
func GetXXX() *XXX{
once.Do(func(){
globalXXX = getXXX()
})
return globalXXX
}
string與slice的結構本質上是一樣的,可以直接強制轉換:
import (
"reflect"
"unsafe"
)
// copy from prometheus source code
// NoAllocString convert []byte to string
func NoAllocString(bytes []byte) string {
return *(*string)(unsafe.Pointer(&bytes))
}
// NoAllocBytes convert string to []byte
func NoAllocBytes(s string) []byte {
strHeader := (*reflect.StringHeader)(unsafe.Pointer(&s))
sliceHeader := reflect.SliceHeader{Data: strHeader.Data, Len: strHeader.Len, Cap: strHeader.Len}
return *(*[]byte)(unsafe.Pointer(&sliceHeader))
}
上面的程式碼可以避免string和[]byte在轉換的時候發生拷貝。
注意:轉換後的物件一定要立即使用,不要進一步參照到更深的層次中去。牢記這是不安全程式碼,謹慎使用。
懂C的人,請繞過……
例如一個[]int64的陣列要轉換為[]uint64的陣列,使用個指標強制轉換就行了。
package main
import (
"testing"
"unsafe"
)
func TestConvert(t *testing.T) {
int64Slice := make([]int64, 0, 100)
int64Slice = append(int64Slice, 1, 2, 3)
uint64Slice := *(*[]uint64)(unsafe.Pointer(&int64Slice))
t.Logf("%+v", uint64Slice)
}
還有一種使用場景,要比較兩個大陣列是否完全一樣:可以把陣列強制轉換為[]byte,然後使用bytes.Compare()。相當於C中的memcmp()函數。
類似的操作還很多,推薦這篇文章:《深度解密Go語言之unsafe》
模糊記得一個golang(或是rust)的原則:
普通開發者可以使用安全程式碼來無顧慮的使用,高手把不安全程式碼包裝成安全程式碼來提供高效能元件。
相比C的陣列存取,為什麼golang可以做到很安全?
答案是編譯器加了兩條越界檢查的指令。每次通過下標存取陣列,就像這樣:
if index<0 || index>=len(slice){
panic("out of index")
}
return slice[index]
這兩條越界檢查指令是有開銷的,請看我的測試:《golang中陣列邊界檢查的開銷大約是1.87%~3.12%》
所以,當某些位置使用類似查表法的時候,可以用不安全程式碼繞過越界檢查:
slice := make([]byte, 1024*1024)
offset = 100
b := (*(*byte)(unsafe.Pointer(uintptr(unsafe.Pointer(&arr[0])) + uintptr(offset))))
理論上,每個新版的golang,都有一定編譯器優化的提升。
-X importpath.name=value 編譯期設定變數的值
-s disable symbol table 禁用符號表
-w disable DWARF generation 禁用偵錯資訊
理論上說 -s -w加上後,程式碼段的長度會減小,理論上會提高CPU程式碼cache的利用率。(還未親自測試過)
runtime中有的底層函數是組合實現的,效能很高,但是不是export型別。
這時候可以用連結宣告來使用這些函數:
//go:noescape
//go:linkname memmove runtime.memmove
//goland:noinspection GoUnusedParameterfunc memmove(to unsafe.Pointer, from unsafe.Pointer, n uintptr)
func memmove(to, from unsafe.Pointer, n uintptr)
// 通過上面的宣告後,就可以在程式碼中使用底層的memmove函數了。這個函數相當於c中的memcpy()
具體的細節請看這篇文章:《Go的2個黑魔法技巧》(騰訊 pedrogao)
golang的小函數預設就是內聯的。
可以通過函數前的註釋 //go:noinline
來取消內聯,不過似乎沒有理由這麼做。
關於函數內聯的深層知識還是值得學習的,推薦這篇文章:《詳解Go內聯優化》
可以關注文章中的這個內聯優化技巧:
可通過
-gcflags="-l"
選項全域性禁用內聯,與一個-l
禁用內聯相反,如果傳遞兩個或兩個以上的-l
則會開啟內聯,並啟用更激進的內聯策略。
golang 1.18正式釋出了泛型。
泛型可以讓之前基於反射的程式碼變得更加簡單,很多type assert的程式碼可以去掉;基於interface的執行期動態分發,也可以轉成編譯期決定。
由於對具體的型別產生了具體的程式碼,理論上指令cache命中會提高,分支預測失敗會降低,
不過,對於有一定體量的golang團隊而言,泛型的引入要考慮的問題比較多:如何避免濫用,如何找到與之匹配的基礎庫?
在整個團隊的能力還沒準備好迎接泛型以前,使用工具生產程式碼的產生式程式設計
或許是更容易駕馭的方法。
編譯期決定當然是好於執行期決定的。
我的建議是:
有的場景下,標準庫提供的API不夠好。下面列舉一些自己認識的fast-xx元件。
原始碼請見:https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/lib/fasttime/fasttime.go
原理就是建立協程每秒一次獲取 time.Now(),然後一秒以內取時間戳就只是存取全域性變數。
我測試過:效能比直接使用time.Now()快三倍左右。
原始碼請見:https://github.com/valyala/fastrand
假設一次要輸出幾兆位元組的JSON字串,如何優化效能?
VictoriaMetrics中的vm-select就遇到了這個問題,當一個大查詢需要返回很多的metrics資料的時候,其輸出的json的體積非常可觀。
如果把資料先放到一個大陣列,再使用json.Marsharl,則一方面要頻繁申請釋放記憶體,另一方面會帶來記憶體使用量的劇烈抖動。vm-select的解決方式是使用quicktemplate庫——把json看成是字串流的輸出。
具體程式碼請看:https://github.com/valyala/quicktemplate
總有很多人想把某個細分領域做到極致:
歡迎推薦更好好用的庫給我,謝謝。
一些涉及大量計算的熱點,可以採用組合來優化。
golang使用plan 9組合的語法,門檻還是比較高的。(經過半年斷斷續續的學習,我已經知道怎麼看註釋了)
所幸的是,懂C的人可以通過工具一步步把C程式碼翻譯成plan 9組合。
我自己做了個嘗試:《玩一玩golang組合》(師從於這篇:《Go的2個黑魔法技巧》)
注意:https://github.com/Maratyszcza/PeachPy這個庫的程式碼翻譯能力有限,我就發現有的程式碼無法翻譯的情況。
且,只支援amd64平臺下的翻譯。
如果大家遇到更好的組合翻譯工具,請推薦給我。
使用組合的最佳理由是SIMD指令集。
通常,一條指令只處理一條資料。而simd中,一條指令可以處理多條資料,當資料由多個128bit或者256bit構成的時候,使用SIMD指令可以取得較好的收益。
以strcmp()函數為例,傳統的寫法是逐個字元比較;而使用SIMD的話,可以把連續的16位元組或者32位元組(AVX2) load 到暫存器中,然後一次性比較。
這塊知識體系較為龐大,有興趣請自行搜尋。
當前流行的OLAP資料庫clickhouse為何效能如此卓絕?其兩個核心技術點就是SIMD和JIT。
在計算機技術中,即時編譯(英語:just-in-time compilation,縮寫為JIT;又譯及時編譯[1]、實時編譯[2]),也稱為動態翻譯或執行時編譯[3],是一種執行計算機程式碼的方法,這種方法涉及在程式執行過程中(在執行期)而不是在執行之前進行編譯。[4]通常,這包括原始碼或更常見的位元組碼到機器碼的轉換,然後直接執行。實現JIT編譯器的系統通常會不斷地分析正在執行的程式碼,並確定程式碼的某些部分,在這些部分中,編譯或重新編譯所獲得的加速將超過編譯該程式碼的開銷。
JIT編譯是兩種傳統的機器程式碼翻譯方法——提前編譯(英語:ahead-of-time compilation)(AOT)和解釋——的結合,它結合了兩者的優點和缺點。[4]大致來說,JIT編譯,以直譯器的開銷以及編譯和連結(解釋之外)的開銷,結合了編譯程式碼的速度與解釋的靈活性。JIT編譯是動態編譯的一種形式,允許自適應優化(英語:adaptive optimization),比如動態重編譯和特定於微架構的加速[nb 1][5]——因此,在理論上,JIT編譯比靜態編譯能夠產生更快的執行速度。解釋和JIT編譯特別適合於動態程式語言,因為執行時系統可以處理後期繫結(英語:Late binding)的資料型別並實施安全保證。
——維基百科-即時編譯
JIT在JAVA圈耳熟能詳,通常指把位元組碼編譯為機器碼。但是golang沒有機器碼,所以golang中JIT並不用於位元組碼翻譯。
我覺得golang中的JIT可以這樣定義:為特定的功能點,動態生成特定的機器碼,以提高程式效能。
關於如何實現一個golang中的JIT,可以閱讀這篇:《使用 Go 語言寫一個即時編譯器(JIT)》
像把大象放進冰箱裡一樣總結一下:
1.把一些機器碼,放到一個陣列中;(已經知道這些機器碼是幹啥的了)
2.使用mmap系統呼叫分配一塊記憶體,把記憶體設定為可執行,把上面的機器碼拷貝進去;(然後這片記憶體就成為了程式的程式碼段)
3.定義一個函數指標指向mmap的記憶體;
4.執行函數。
也有golang庫提供動態生成機器碼的能力:https://github.com/goccy/go-jit。支援的指令有限,而且,猜測沒人願意這麼寫程式碼。
(讀者一定在想這麼雞肋的東西介紹給我幹啥……)
golang的JIT的一個精彩應用是bytedance開源的sonic庫,從測試資料來看,應該是golang圈子裡最快的JSON解析庫。
怎麼做到的呢?
例如有這樣一個json:
{"a":123, "b":"abc"}
要把它解析到結構體:
type Data struct{
A int64
B string
}
一般來說,這個過程需要很多的判斷:源欄位名是什麼?源欄位什麼型別?目的欄位名的反射物件在哪裡?目的物件的記憶體指標在哪裡?如果想要讓解析過程變快,最好是直接去掉這些判斷:遇到"a", 在目的記憶體的偏移位置0,寫入8位元組整形值……
但是上面的做法又沒有通用性。如何直接的解析一個型別,又滿足通用性?JIT就是個好辦法。
針對型別Data
,通過JIT產生一段最直接最高效的解析程式碼,並且以後都通過這段程式碼來解析。進而推演到每個型別都有專門的解析程式碼。如此:針對特定結構,有特定的最優解析程式碼。這樣的做法絕對是最優的,無法被別的方法超越。
就像ClickHouse一樣,相信未來會有越來越多的系統應用會添置JIT的能力。
關於cgo的效能,我認為主要是golang runtime中的物理執行緒(GMP模型中的M),與執行CGO的物理執行緒之間的通訊造成了遠高於直接函數呼叫的損耗。
內部顯示 如果是單純的 emtpy call,使用 cgo 耗時 55.9 ns/op, 純 go 耗時 0.29 ns/op,相差了 192 倍。
而實際上我們在使用 cgo 的時候不太可能進行空呼叫,一般來說會把效能影響較大,計算耗時較長的計算放在 cgo 中,如果是這種情況,每次呼叫額外 55.9 ns 的額外耗時應該是可以接受的存取。
golang為了保障runtime的協程排程不被阻塞,就需要所有被排程的協程函數都是不阻塞的。一旦加入CGO,就無法保障函數不阻塞了,因此只有額外開闢物理執行緒來執行CGO的函數。
這裡特別需要注意的一個坑是:
呼叫CGO的次數越多,時間越長,golang runtime開啟的物理執行緒就越多。
我曾在VictoriaNetrics中的vm-storage中發現,因為大量呼叫ZSTD壓縮庫,導致物理執行緒數是允許核數的10倍。
並且,在目前的golang版本中,這些物理執行緒沒有明確的銷燬機制。
遠多餘可用核數的物理執行緒,會導致大量CPU時間消耗在無意義的執行緒切換上。建議運營中加上runtime的metric上報,一旦發現物理執行緒過多,定期重啟來減少這種損耗。
不要用panic來反饋異常,不要用recover()來接收異常。
除了程式初始化的錯誤,不要在業務的任何地方使用panic。
對於錯誤,存在可預見的error,和不可預見的panic。絕大多數情況都要通過error來針對性的識別並管理錯誤。recover()僅僅用於維護框架穩定的非預期的錯誤捕獲。
目前還未測試過使用recover()是否會導致效能受損。
就我閱讀VictoriaMetrics的原始碼看來,他們一個recover()都沒用——也就是說,他們自信的認為元件只會產生可預見的error。
如果我們處處都想著加上recover()來捕獲panic,是否意味著設計和測試上存在問題?
VictoriaMetrics中,幾乎所有的for迴圈都是一種風格:
var slice []int64
for i := range slice{
item := &slice[i]
}
我想這就是為了避免for迴圈中的第二個變數產生拷貝。就如同寫C/C++的人,for迴圈中的迴圈變數要求寫成 ++i
而不是 i++
。規範好寫法,避免在細節之處有不必要的損耗。
golang中宣告的每個變數預設都是位元組對齊的,這點很好。
需要額外注意兩點:
這種優化點很難找。
關於分支預測的案例,可以看看我寫的這個分析文章:《用重複寫入代替if判斷,減少程式分支》
golang標準庫中也有個很好的例子:《How does ConstantTimeByteEq work?》
一個簡單的if x==y,考慮了攻擊者對計算時間的猜測,考慮了分支預測的損耗。
其他的關於分支預測的優化技巧,這篇也不錯:《淺談利用分支預測提高效率》
在日常的開發中,換個寫法是有可能會提高效能的:
switch variable{
case "a": // 根據業務特點,把最可能的分支放在最前。提高分支預測的成功率
// do something
case "b":
// do something
}
OK,文章到這裡就結束了。
本人也才寫了兩年的golang,難免有很多錯誤之處,還請讀者不吝賜教,謝謝!