八招解決 Golang 性能問題
1. 明智使用 Goroutine
你肯定習慣通過 goroutine 同時運行函數(shù),覺得很酷,對吧?但實際上太多 goroutine 會拖慢運行速度,每個 goroutine 都會占用一定內(nèi)存,如果程序運行了數(shù)百萬個 goroutine,就會增加很多內(nèi)存。
避免以下做法:
for _, item := range bigList {
go process(item)
}
試著用信號量(semaphore)進行限制:
sem := make(chan struct{}, 100) // 限制 100 個 goroutines
for _, item := range bigList {
sem <- struct{}{}
go func(i Item) {
defer func() { <-sem }()
process(i)
}(item)
我曾經(jīng)做過航班搜索項目,該項目具有數(shù)百萬的搜索規(guī)模。我們在很多地方都添加了 goroutine,試圖對其進行優(yōu)化。這時我才意識到 goroutine 會占用大量內(nèi)存。我們需要持續(xù)監(jiān)控每個點上運行的 goroutine 的數(shù)量,并需要建立某種機制,根據(jù)性能要求對其進行調(diào)整。
2. 謹慎使用 channel
channel 非常適合用于程序之間通信,但也可能很棘手。不帶緩沖的 channel 會導致程序在意想不到的時候阻塞。
避免以下做法:
ch := make(chan int)
在需要的時候使用帶緩沖的 channel:
ch := make(chan int, 100) // 緩沖大小為 100
帶緩沖的 channel 可確保發(fā)送方在接收方尚未準備好接受任何數(shù)據(jù)的情況下不會被卡住,可以幫助我們簡化流程,尤其是在處理大規(guī)模事務(wù)時,通道可能會非常繁忙。
3. 避免使用全局變量
全局變量初看似乎很簡單,但可能會帶來很多問題。全局變量會讓人很難跟蹤變化的內(nèi)容,并可能導致錯誤。
避免以下做法:
var counter int
func increment() {
counter++
}
傳遞變量:
func increment(counter int) int {
return counter + 1
}
counter := 0
counter = increment(counter)
曾經(jīng)看到過遍布全局變量的代碼,調(diào)試時完全是一場噩夢。保持局部化會讓代碼更簡潔、更快速。
4. 高效使用切片
切片是 Golang 中使用最多的數(shù)據(jù)結(jié)構(gòu)之一,因此我們有責任明智的使用。一不小心,就可能會在切片中拷貝更多數(shù)據(jù)。
避免不必要的拷貝:
// 不好: 創(chuàng)建新切片
newSlice := make([]int, len(oldSlice))
copy(newSlice, oldSlice)
// 更好: 只使用原始切片
newSlice := oldSlice[:]
此外,在對切片添加數(shù)據(jù)時,如果知道容量會有多大,請考慮預先分配容量。
預先分配容量:
s := make([]int, 0, 100) // 容量為 100
這有助于避免多次分配內(nèi)存,加快速度。
5. 分析代碼
需要采取某種機制來檢查代碼與模塊的效率,Go 內(nèi)置工具可以幫助查看程序把時間花在了哪些方面。
使用分析工具(profiler):
import (
"runtime/pprof"
"os"
)
func main() {
f, _ := os.Create("cpu.prof")
pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile()
// 業(yè)務(wù)代碼
}
運行分析工具后,可能會發(fā)現(xiàn)某個你認為很快的函數(shù)實際上拖慢了速度。修復后,程序可以運行得更快了。
6. 使用標準庫
Go 有一些漂亮的庫,而我犯的一個錯誤就是在項目中引入了很多 Go 已經(jīng)提供的功能。強烈建議你在編寫代碼之前,先檢查一下 Go 是否已經(jīng)提供了相應功能。
避免編寫自己的排序功能:
func mySort(data []int) {
// 自定義排序
}
使用內(nèi)置排序軟件包:
import "sort"
sort.Ints(data)
以前我經(jīng)常重新發(fā)明輪子,但現(xiàn)在更多依賴標準庫,這樣代碼可以更簡短,通常也更快。
7. 注意內(nèi)存分配
分配內(nèi)存需要時間。如果在短時間內(nèi)創(chuàng)建許多臨時對象,考慮到創(chuàng)建對象所耗費的時間,很可能會影響性能。
盡可能復用對象:
var buffer bytes.Buffer
for i := 0; i < 1000; i++ {
buffer.Reset()
buffer.WriteString("Some data")
process(buffer.Bytes())
}
通過重復使用 buffer,可以避免每次都分配新的內(nèi)存,這個技巧可以加快運行多次的循環(huán)速度。
8. 隨時更新 Go 版本
Go 的每個新版本都會對性能進行改進,總是使用最新的 Go 版本,就能免費獲得這些優(yōu)勢。
因為更新看起來會很麻煩,因此很多人會一直使用舊版本,但升級后很有可能會發(fā)現(xiàn)程序在不修改任何代碼的情況下運行得更快了!
總結(jié)
這些是多年來積累的一些小技巧。每個項目都有所不同,請確保解決方案確實能夠解決你的問題,而每當面臨某種性能瓶頸時,請注意上述提到的方面。
最后,在修改代碼后,一定要記得對代碼進行測試和性能分析。有時,我們認為的優(yōu)化可能達不到預期效果。