Go語(yǔ)言中常見(jiàn)的坑以及高性能編程技巧分享
背景
代碼的穩(wěn)健性、高性能、可讀性是我們每一位coder必須去追求的目標(biāo),也是coding的基本功。
本文結(jié)合Go語(yǔ)言的特性,以及自己在寫(xiě)Go項(xiàng)目中做的總結(jié),從Go常見(jiàn)的數(shù)據(jù)結(jié)構(gòu)、內(nèi)存管理、并發(fā)等方面做了相關(guān)總結(jié)
本文相關(guān)代碼的驗(yàn)證環(huán)境
GOARCH="arm64" GOOS="darwin" GOVERSION="go1.16.15"
一. 常見(jiàn)的坑
1.1 數(shù)據(jù)結(jié)構(gòu)
1.在函數(shù)調(diào)用過(guò)程中,數(shù)組是值傳遞pass-by-value,必要時(shí)必須使用slice。因?yàn)閟lice是引用類(lèi)型,在slice傳參時(shí),只會(huì)復(fù)制slice的Data指針和len、cap,形參和實(shí)參的slice使用的是同一個(gè)底層數(shù)組。
2.map是一種hash表實(shí)現(xiàn),每次遍歷的順序都可能不一樣。
3.切片會(huì)導(dǎo)致整個(gè)數(shù)組被鎖定,導(dǎo)致底層數(shù)組無(wú)法及時(shí)釋放內(nèi)存,如果底層數(shù)組過(guò)大,會(huì)對(duì)內(nèi)存產(chǎn)生極大的壓力。
錯(cuò)誤示例:
func test() { fileHeaderMap := make(map[string][]byte) for i := 0; i < 5; i++ { name := "/data/test/file_"+strconv.Itoa(i) data, err := ioutil.ReadFile(name) if err != nil { fmt.Println(err) continue } fileHeaderMap[name] = data[:1] } // do some thing }
正確示例:
解決辦法是將結(jié)果克隆一份,這樣可以釋放底層數(shù)組
func test() { fileHeaderMap := make(map[string][]byte) for i := 0; i < 5; i++ { name := "/data/test/file_"+strconv.Itoa(i) data, err := ioutil.ReadFile(name) if err != nil { fmt.Println(err) continue } fileHeaderMap[name] = append([]byte{}, data[:1]...) } // do some thing }
4.當(dāng)函數(shù)的可變參數(shù)是空接口時(shí),傳入空接口的切片時(shí)需要注意參數(shù)展開(kāi)的問(wèn)題
func main() { var a = []interface{}{111, 222, 333} fmt.Println(a) fmt.Println(a...) } // 不管是否展開(kāi),編譯器都會(huì)編譯通過(guò),但是輸出是不同的: //print: [111 222 333] 111 222 333
5.對(duì)于切片的操作,會(huì)操作同一個(gè)底層數(shù)組,因此對(duì)于一個(gè)切片的修改操作,會(huì)影響到整個(gè)數(shù)組。另外由于string 在go中是immutable,對(duì)于同一個(gè)字符串的兩個(gè)變量,Go做了內(nèi)存優(yōu)化,會(huì)使用的相同的底層數(shù)組
func main() { slice := []int{1, 2, 3, 4, 5} slice1 := slice[:2] slice2 := slice[:4] fmt.Println("slice: ", slice, ",slice1: ", slice1, ",slice2: ", slice2) slice2[0] = 6 fmt.Println("slice: ", slice, ",slice1: ", slice1, ",slice2: ", slice2) string1 := "go hello word" string2 := "go hello word" fmt.Printf("string1 data addr: %d \n", (*reflect.StringHeader)(unsafe.Pointer(&string1)).Data) fmt.Printf("string2 data addr: %d \n", (*reflect.StringHeader)(unsafe.Pointer(&string2)).Data) } //print: slice: [1 2 3 4 5] ,slice1: [1 2] ,slice2: [1 2 3 4] slice: [6 2 3 4 5] ,slice1: [6 2] ,slice2: [6 2 3 4] string1 data addr: 4333475958 string2 data addr: 4333475958
1.2 Go語(yǔ)言特性相關(guān)
1.Go中的recover捕獲的是祖父級(jí)調(diào)用時(shí)候的panic,直接調(diào)用時(shí)不會(huì)有任何效果,必須在defer函數(shù)中調(diào)用才有效果。
func test() err error { defer func() { if r := recover(); r != nil { err = fmt.Errorf("[ERROR] Process exception. Panic: %v", r) } }() // do some thing panic("some error happend") }
2.不同goroutine之間不滿(mǎn)足順序一致性的內(nèi)存模型,需要使用顯示的同步:如 channel
var testMsg string var done = make(chan struct{}) func initMsg() { testMsg = "go hello world" done <- struct{}{} } func main() { go initMsg() <-done println(testMsg) }
3.閉包的錯(cuò)誤使用,導(dǎo)致使用同一個(gè)變量
錯(cuò)誤示例:
func main() { for i := 0; i < 5; i++ { defer func() { println(i) }() } } //print: 5 5 5 5 5
正確示例:
func main() { for i := 0; i < 5; i++ { defer func(i int) { println(i) }(i) // or //i := i //defer func(){ // println(i) //}() } } //print: 4 3 2 1 0
4.因?yàn)閐efer在函數(shù)退出時(shí)才會(huì)執(zhí)行,因此不能在循環(huán)內(nèi)部執(zhí)行defer,否則會(huì)導(dǎo)致相關(guān)defer操作調(diào)用延遲,比如fd會(huì)延遲關(guān)閉,應(yīng)將defer包裝在func中
錯(cuò)誤示例:
func test() { for i := 0; i < 5; i++ { f, err := os.Open("/data/test/file_"+strconv.Itoa(i)) if err != nil { fmt.Println(err) continue } defer f.Close() // do some thing } // do some thing }
正確示例:
func test() { for i := 0; i < 5; i++ { func() { f, err := os.Open("/data/test/file_"+strconv.Itoa(i)) if err != nil { fmt.Println(err) return } defer f.Close() // do some thing }() } // do some thing }
5.Goroutine泄漏:
Go語(yǔ)言是帶內(nèi)存自動(dòng)回收的特性,因此內(nèi)存一般不會(huì)泄漏。但是Goroutine卻存在泄漏的情況,同時(shí)泄漏的Goroutine引用的內(nèi)存也同樣無(wú)法被回收。因此可通過(guò)context包或者退出的channel來(lái)避免Goroutine的泄漏。
錯(cuò)誤示例:
func main() { ch := func() <-chan int { ch := make(chan int) go func() { for i := 0; ; i++ { ch <- i } } () return ch }() for value := range ch { fmt.Println(value) if value == 10 { break } } // do some thing }
上面的程序中后臺(tái)Goroutine向channel輸入整數(shù),main函數(shù)中輸出該整數(shù)。但是當(dāng)值為10時(shí), break跳出for循環(huán),后臺(tái)Goroutine就處于無(wú)法被回收的狀態(tài)了
正確示例:
func main() { ctx, cancel := context.WithCancel(context.Background()) // stopChan := make(chan struct{}) //ch := func(stopChan chan struct{}) <-chan int { ch := func(ctx context.Context) <-chan int { ch := make(chan int) go func() { for i := 0; ; i++ { select { //case <-stopChan: // return case <- ctx.Done(): return case ch <- i: } } }() return ch }(ctx) //}(stopChan) for value := range ch { fmt.Println(value) if value == 10 { cancel() //通過(guò)調(diào)用cancel()來(lái)通知后臺(tái)Goroutine退出 // stopChan <- struct{}{} break } } // do some thing }
6. go channel注意事項(xiàng)
- 如果是一個(gè)buffer channel,即使被 close, 也可以讀到之前存入的值,讀取完畢后開(kāi)始讀零值,已經(jīng)關(guān)閉的channel寫(xiě)入則會(huì)觸發(fā) panic
- nil channel 讀取和存入都會(huì)阻塞,close 會(huì) panic
- 已經(jīng)close過(guò)的channel再次close會(huì)觸發(fā)panic
- 關(guān)閉channel的原則:不要在消費(fèi)端進(jìn)行關(guān)閉、不要在有多個(gè)并行的生產(chǎn)端關(guān)閉
二. 高性能Go編程
2.1 數(shù)據(jù)結(jié)構(gòu)
盡可能指定容器容量,以便為容器預(yù)先分配內(nèi)存。這將在后續(xù)添加元素時(shí)減少通過(guò)復(fù)制來(lái)調(diào)整容器大小
遍歷 []struct{} 使用下標(biāo)而不是 range
如果切片是[]int,切片的Item為int差別不大,但是如果切片Item是一個(gè)結(jié)構(gòu)體類(lèi)型struct{}時(shí),且Item中包含一些比較大內(nèi)存的成員類(lèi)型,比如 [2048]byte,如果每次遍歷[]struct{} ,都會(huì)進(jìn)行一次值拷貝,所以會(huì)帶來(lái)性能消耗。此外,因?yàn)?range 遍歷事獲取值拷貝的副本,所以對(duì)副本的修改,是不會(huì)影響到原切片。 如果切片的Item是指針類(lèi)型,即[]*struct{} 則兩者遍歷方法都一樣
string轉(zhuǎn)[]byte的零拷貝操作:
我們一般在做string轉(zhuǎn)[]byte時(shí),會(huì)直接進(jìn)行[]byte(string),這樣的話(huà)會(huì)發(fā)生一次拷貝操作,對(duì)于一些長(zhǎng)尾的字符串,會(huì)產(chǎn)生性能問(wèn)題。這是因?yàn)樵贕o的設(shè)計(jì)中string是immutable,而[]byte是mutable,如果不希望進(jìn)行這次拷貝的消耗,這時(shí)候就需要用到unsafe
可參見(jiàn):pkg.go.dev/reflect#SliceHeader
func string2bytes(s string) []byte { stringHeader := (*reflect.StringHeader)(unsafe.Pointer(&s)) var b []byte rs := (*reflect.SliceHeader)(unsafe.Pointer(&b)) rs.Data = stringHeader.Data rs.Len = stringHeader.Len rs.Cap = stringHeader.Len return b }
不過(guò)需要特別注意這樣生成的[]byte不可修改,否則會(huì)出現(xiàn)未定義的行為
Go 中空結(jié)構(gòu)體 struct{} 是不占用內(nèi)存空間,因此fmt.Println(unsafe.Sizeof(struct{}{})) 為0,不像 C/C++ 中空結(jié)構(gòu)體仍占用 1 字節(jié)大小,因此可用于以下幾個(gè)場(chǎng)景來(lái)節(jié)省內(nèi)存:
- 在實(shí)現(xiàn)集合時(shí),可以將map的value的設(shè)置為struct{}來(lái)節(jié)省內(nèi)存;
- 在使用不發(fā)送數(shù)據(jù)的channel時(shí),只是用于通知其他Goroutine,也可以使用空結(jié)構(gòu)體;
- 僅包含方法的結(jié)構(gòu)體,即結(jié)構(gòu)體沒(méi)有任何成員類(lèi)型字段
盡量少使用反射,因?yàn)榉瓷淅镞厾砍兜筋?lèi)型判斷和內(nèi)存分配,會(huì)對(duì)性能產(chǎn)生影響
2.2 內(nèi)存管理
struct結(jié)構(gòu)體內(nèi)的成員布局需要考慮內(nèi)存對(duì)齊,一般建議字段寬度從小到大由上到下排列,減少內(nèi)存占用,提高內(nèi)存讀寫(xiě)性能
值傳遞會(huì)拷貝整個(gè)對(duì)象,而指針傳遞只會(huì)拷貝對(duì)象的地址,指針指向的對(duì)象是同一個(gè)。返回指針可以減少值的拷貝,但是會(huì)導(dǎo)致內(nèi)存分配逃逸到堆中,即變量逃逸,增加垃圾回收(GC)的負(fù)擔(dān)。在對(duì)象頻繁創(chuàng)建和刪除的場(chǎng)景下,傳遞指針會(huì)導(dǎo)致GC的開(kāi)銷(xiāo)增大,影響性能。
一般情況下,對(duì)于需要修改原對(duì)象值,或占用內(nèi)存比較大的結(jié)構(gòu)體,選擇返回指針。對(duì)于只讀的或者占用內(nèi)存較小的結(jié)構(gòu)體,直接返回值性能要優(yōu)于指針
對(duì)于需要重復(fù)分配、回收內(nèi)存的地方,優(yōu)先使用sync.Pool進(jìn)行池化,用來(lái)保存和復(fù)用對(duì)象,減少內(nèi)存分配,降低GC壓力,并且sync.Pool是并發(fā)安全的
2.3 并發(fā)編程
1.鎖的使用
- 優(yōu)先考慮無(wú)鎖的數(shù)據(jù)結(jié)構(gòu)使用,即lock-free 比如atomic包,但其底層也是memory barrier,如果并發(fā)太高,性能也未必高
- Goroutine局部,即線程的TLS,最后再進(jìn)行每個(gè)Goroutine合并處理
- 進(jìn)行數(shù)據(jù)切片,減少鎖的競(jìng)爭(zhēng),或者使用讀寫(xiě)鎖,替換互斥鎖
- 對(duì)于map數(shù)據(jù)結(jié)構(gòu)的并發(fā)訪問(wèn),在讀多寫(xiě)少的情況下,建議使用官方的sync.Map,sync.Map是空間換時(shí)間的實(shí)現(xiàn)內(nèi)部有兩個(gè)map,有一個(gè)專(zhuān)門(mén)讀的read map,另一個(gè)是提供讀寫(xiě)的dirty map,優(yōu)先讀read map,未讀到會(huì)穿透到dirty map,但是不適用于大量寫(xiě)的場(chǎng)景,dirty map會(huì)存在頻繁刷新為read map,整體性能會(huì)降低。
2.Goroutine 池化 github.com/panjf2000/ants
到此這篇關(guān)于Go語(yǔ)言中常見(jiàn)的坑以及高性能編程技巧分享的文章就介紹到這了,更多相關(guān)Go編程技巧內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
golang jsoniter extension 處理動(dòng)態(tài)字段的實(shí)現(xiàn)方法
這篇文章主要介紹了golang jsoniter extension 處理動(dòng)態(tài)字段的實(shí)現(xiàn)方法,我們使用實(shí)例級(jí)別的 extension, 而非全局,可以針對(duì)不同業(yè)務(wù)邏輯有所區(qū)分,jsoniter 包提供了比較完善的定制能力,通過(guò)例子可以感受一下擴(kuò)展性,需要的朋友可以參考下2023-04-04go+redis實(shí)現(xiàn)消息隊(duì)列發(fā)布與訂閱的詳細(xì)過(guò)程
這篇文章主要介紹了go+redis實(shí)現(xiàn)消息隊(duì)列發(fā)布與訂閱,redis做消息隊(duì)列的缺點(diǎn):沒(méi)有持久化,一旦消息沒(méi)有人消費(fèi),積累到一定程度后就會(huì)丟失,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-09-09Golang極簡(jiǎn)入門(mén)教程(三):并發(fā)支持
這篇文章主要介紹了Golang極簡(jiǎn)入門(mén)教程(三):并發(fā)支持,本文講解了goroutine線程、channel 操作符等內(nèi)容,需要的朋友可以參考下2014-10-10Go內(nèi)存節(jié)省技巧簡(jiǎn)單實(shí)現(xiàn)方法
這篇文章主要為大家介紹了Go內(nèi)存節(jié)省技巧簡(jiǎn)單實(shí)現(xiàn)方法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-01-01Go語(yǔ)言使用goroutine及通道實(shí)現(xiàn)并發(fā)詳解
這篇文章主要為大家介紹了Go語(yǔ)言使用goroutine及通道實(shí)現(xiàn)并發(fā)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08Go語(yǔ)言開(kāi)發(fā)發(fā)送Get和Post請(qǐng)求的示例
這篇文章主要介紹了Go語(yǔ)言開(kāi)發(fā)發(fā)送Get和Post請(qǐng)求的示例,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-07-07