高質量編程與性能調優(yōu)實戰(zhàn)
概述
介紹編碼規(guī)范,幫助大家寫出高質量程序
介紹 Go 語言的性能優(yōu)化建議,分析對比不同方式對性能的影響和背后的原理
講解常用性能分析工具 pprof 的使用和工作原理,熟悉排查程序性能問題的基本流程
分析性能調優(yōu)實際案例,介紹實際性能調優(yōu)時的工作內容
內容概要
[圖片上傳失敗...(image-9a539e-1675238053934)]
實踐準備 (必須)
克隆 github.com/wolfogre/go… 到本地,保證能夠編譯運行
嘗試使用 test 命令,編寫并運行簡單測試 go.dev/doc/tutoria…
嘗試使用 -bench 參數(shù),對編寫的函數(shù)進行性能測試,pkg.go.dev/testing#hdr…
推薦閱讀
Go 代碼 Review 建議github.com/golang/go/w…
Uber 的 Go 編碼規(guī)范,github.com/uber-go/gui…
高質量編程
簡介
編寫的代碼能夠達到正確可靠、簡潔清晰、無性能隱患的目標就能稱之為高質量代碼
實際應用場景千變萬化,各種語言的特性和語法各不相同,但是高質量編程遵循的原則是相通的
高質量的編程需要注意以下原則:簡單性、可讀性、生產力
常見編碼規(guī)范
代碼格式
- 使用 gofmt 自動格式化代碼,保證所有的 Go 代碼與官方推薦格式保持一致
總結
- 提升可讀性,風格一致的代碼更容易維護、需要更少的學習成本、團隊合作成本,同時可以降低 Review 成本
注釋
-
注釋應該解釋代碼作用
- 適合注釋公共符號,github.com/golang/go/b…
-
注釋應該解釋代碼如何做的
- 適合注釋方法,github.com/golang/go/b…
-
注釋應該解釋代碼實現(xiàn)的原因
- 解釋代碼的外部因素,github.com/golang/go/b…
注釋應該解釋代碼什么情況會出錯
-
公共符號始終要注釋
- 包中聲明的每個公共的符號:變量、常量、函數(shù)以及結構都需要添加注釋
- github.com/golang/go/b…
- github.com/golang/go/b…
總結
代碼是最好的注釋
注釋應該提供代碼未表達出的上下文信息
命名規(guī)范
-
variable
- 簡潔勝于冗長
- 縮略詞全大寫,但當其位于變量開頭且不需要導出時,使用全小寫
- 變量距離其被使用的地方越遠,則需要攜帶越多的上下文信息
- 全局變量在其名字中需要更多的上下文信息,使得在不同地方可以輕易辨認出其含義
-
function
- 函數(shù)名不攜帶包名的上下文信息,因為包名和函數(shù)名總是成對出現(xiàn)的
- 函數(shù)名盡量簡短
- 當名為 foo 的包某個函數(shù)返回類型 Foo 時,可以省略類型信息而不導致歧義
- 當名為 foo 的包某個函數(shù)返回類型 T 時(T 并不是 Foo),可以在函數(shù)名中加入類型信息
-
package
- 只由小寫字母組成。不包含大寫字母和下劃線等字符
- 簡短并包含一定的上下文信息。例如 schema、task 等
- 不要與標準庫同名。例如不要使用 sync 或者 strings
總結
關于命名的大多數(shù)規(guī)范核心在于考慮上下文
人們在閱讀理解代碼的時候也可以看成是計算機運行程序,好的命名能讓人把關注點留在主流程上,清晰地理解程序的功能,避免頻繁切換到分支細節(jié),增加理解成本
控制流程
避免嵌套,保持正常流程清晰
如果兩個分支中都包含 return 語句,則可以去除冗余的 else
-
盡量保持正常代碼路徑為最小縮進,優(yōu)先處理錯誤情況/特殊情況,并盡早返回或繼續(xù)循環(huán)來減少嵌套,增加可讀性
- Go 公共庫的代碼
- github.com/golang/go/b…
總結
線性原理,處理邏輯盡量走直線,避免復雜的嵌套分支
提高代碼的可讀性
錯誤和異常處理
-
簡單錯誤處理
- 優(yōu)先使用 errors.New 來創(chuàng)建匿名變量來直接表示該錯誤。有格式化需求時使用 fmt.Errorf
- github.com/golang/go/b…
-
錯誤的 Wrap 和 Unwrap
在 fmt.Errorf 中使用 %w 關鍵字來將一個錯誤 wrap 至其錯誤鏈中
Go1.13 在 errors 中新增了三個新 API 和一個新的 format 關鍵字,分別是 errors.Is、errors.As 、errors.Unwrap 以及 fmt.Errorf 的 %w。如果項目運行在小于 Go1.13 的版本中,導入 golang.org/x/xerrors 來使用。以下語法均已 Go1.13 作為標準。
-
錯誤判定
- 使用 errors.Is 可以判定錯誤鏈上的所有錯誤是否含有特定的錯誤。
- github.com/golang/go/b…
- 在錯誤鏈上獲取特定種類的錯誤,使用 errors.As
- github.com/golang/go/b…
-
panic
- 不建議在業(yè)務代碼中使用 panic
- 如果當前 goroutine 中所有 deferred 函數(shù)都不包含 recover 就會造成整個程序崩潰
- 當程序啟動階段發(fā)生不可逆轉的錯誤時,可以在 init 或 main 函數(shù)中使用 panic
- github.com/Shopify/sar…
-
recover
- recover 只能在被 defer 的函數(shù)中使用,嵌套無法生效,只在當前 goroutine 生效
- github.com/golang/go/b…
- 如果需要更多的上下文信息,可以 recover 后在 log 中記錄當前的調用棧。
- github.com/golang/webs…
總結
panic 用于真正異常的情況
error 盡可能提供簡明的上下文信息,方便定位問題
recover 生效范圍,在當前 goroutine 的被 defer 的函數(shù)中生效
性能優(yōu)化建議
在滿足正確性、可靠性、健壯性、可讀性等質量因素的前提下,設法提高程序的效率
性能對比測試代碼,可參考 github.com/RaymondCode…
-
slice 預分配內存
- 在盡可能的情況下,在使用 make() 初始化切片時提供容量信息,特別是在追加切片時
- 原理
- ueokande.github.io/go-slice-tr…
- 切片本質是一個數(shù)組片段的描述,包括了數(shù)組的指針,這個片段的長度和容量(不改變內存分配情況下的最大長度)
- 切片操作并不復制切片指向的元素,創(chuàng)建一個新的切片會復用原來切片的底層數(shù)組,因此切片操作是非常高效的
- 切片有三個屬性,指針(ptr)、長度(len) 和容量(cap)。append 時有兩種場景:
- 當 append 之后的長度小于等于 cap,將會直接利用原底層數(shù)組剩余的空間
- 當 append 后的長度大于 cap 時,則會分配一塊更大的區(qū)域來容納新的底層數(shù)組
- 因此,為了避免內存發(fā)生拷貝,如果能夠知道最終的切片的大小,預先設置 cap 的值能夠獲得最好的性能
- 另一個陷阱:大內存得不到釋放
- 在已有切片的基礎上進行切片,不會創(chuàng)建新的底層數(shù)組。因為原來的底層數(shù)組沒有發(fā)生變化,內存會一直占用,直到沒有變量引用該數(shù)組
- 因此很可能出現(xiàn)這么一種情況,原切片由大量的元素構成,但是我們在原切片的基礎上切片,雖然只使用了很小一段,但底層數(shù)組在內存中仍然占據(jù)了大量空間,得不到釋放
- 推薦的做法,使用 copy 替代 re-slice
-
map 預分配內存
- 原理
- 不斷向 map 中添加元素的操作會觸發(fā) map 的擴容
- 根據(jù)實際需求提前預估好需要的空間
- 提前分配好空間可以減少內存拷貝和 Rehash 的消耗
- 原理
-
使用 strings.Builder
- 常見的字符串拼接方式
- strings.Builder
- bytes.Buffer
- strings.Builder 最快,bytes.Buffer 較快,+ 最慢
- 原理
- 字符串在 Go 語言中是不可變類型,占用內存大小是固定的,當使用 + 拼接 2 個字符串時,生成一個新的字符串,那么就需要開辟一段新的空間,新空間的大小是原來兩個字符串的大小之和
- strings.Builder,bytes.Buffer 的內存是以倍數(shù)申請的
- strings.Builder 和 bytes.Buffer 底層都是 []byte 數(shù)組,bytes.Buffer 轉化為字符串時重新申請了一塊空間,存放生成的字符串變量,而 strings.Builder 直接將底層的 []byte 轉換成了字符串類型返回
- 常見的字符串拼接方式
-
使用空結構體節(jié)省內存
- 空結構體不占據(jù)內存空間,可作為占位符使用
- 比如實現(xiàn)簡單的 Set
- Go 語言標準庫沒有提供 Set 的實現(xiàn),通常使用 map 來代替。對于集合場景,只需要用到 map 的鍵而不需要值
-
使用 atomic 包
- 原理
- 鎖的實現(xiàn)是通過操作系統(tǒng)來實現(xiàn),屬于系統(tǒng)調用,atomic 操作是通過硬件實現(xiàn)的,效率比鎖高很多
- sync.Mutex 應該用來保護一段邏輯,不僅僅用于保護一個變量
- 對于非數(shù)值系列,可以使用 atomic.Value,atomic.Value 能承載一個 interface{}
- 原理
總結
避免常見的性能陷阱可以保證大部分程序的性能
針對普通應用代碼,不要一味地追求程序的性能,應當在滿足正確可靠、簡潔清晰等質量要求的前提下提高程序性能
性能調優(yōu)實戰(zhàn)
性能調優(yōu)簡介
- 性能調優(yōu)原則
- 要依靠數(shù)據(jù)不是猜測
- 要定位最大瓶頸而不是細枝末節(jié)
- 不要過早優(yōu)化
- 不要過度優(yōu)化
性能分析工具
性能調優(yōu)的核心是性能瓶頸的分析,對于 Go 應用程序,最方便的就是 pprof 工具
-
pprof 功能說明
- pprof 是用于可視化和分析性能分析數(shù)據(jù)的工具
- 可以知道應用在什么地方耗費了多少 CPU、memory 等運行指標 [圖片上傳失敗...(image-8a4f6f-1675238053934)]
-
pprof 實踐
- github.com/wolfogre/go…
- 前置準備,熟悉簡單指標,能夠編譯運行 pprof 測試項目
- 實際分析排查過程
- 排查 CPU 問題
- 命令行分析
- go tool pprof "http://localhost:6060/debug/pprof/profile?seconds=10"
- top 命令
- list 命令
- 熟悉 web 頁面分析
- 調用關系圖,火焰圖
- go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/cpu"
- 命令行分析
- 排查堆內存問題
- go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/heap"
- 排查協(xié)程問題
- go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/goroutine"
- 排查鎖問題
- go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/mutex"
- 排查阻塞問題
- go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/block"
- 排查 CPU 問題
-
pprof 的采樣過程和原理
- CPU 采樣
- 堆內存采樣
- 協(xié)程和系統(tǒng)線程采樣
- 阻塞操作和鎖競爭采樣
性能調優(yōu)案例
-
基本概念
- 服務:能單獨部署,承載一定功能的程序
- 依賴:Service A 的功能實現(xiàn)依賴 Service B 的響應結果,稱為 Service A 依賴 Service B
- 調用鏈路:能支持一個接口請求的相關服務集合及其相互之間的依賴關系
- 基礎庫:公共的工具包、中間件
-
業(yè)務優(yōu)化
- 流程
- 建立服務性能評估手段
- 分析性能數(shù)據(jù),定位性能瓶頸
- 重點優(yōu)化項改造
- 優(yōu)化效果驗證
- 建立壓測評估鏈路
- 服務性能評估
- 構造請求流量
- 壓測范圍
- 性能數(shù)據(jù)采集
- 分析性能火焰圖,定位性能瓶頸
- pprof 火焰圖
- 重點優(yōu)化項分析
- 規(guī)范組件庫使用
- 高并發(fā)場景優(yōu)化
- 增加代碼檢查規(guī)則避免增量劣化出現(xiàn)
- 優(yōu)化正確性驗證
- 上線驗證評估
- 逐步放量,避免出現(xiàn)問題
- 進一步優(yōu)化,服務整體鏈路分析
- 規(guī)范上游服務調用接口,明確場景需求
- 分析業(yè)務流程,通過業(yè)務流程優(yōu)化提升服務性能
- 流程
-
基礎庫優(yōu)化
- 適應范圍更廣,覆蓋更多服務
- AB 實驗 SDK 的優(yōu)化
- 分析基礎庫核心邏輯和性能瓶頸
- 完善改造方案,按需獲取,序列化協(xié)議優(yōu)化
- 內部壓測驗證
- 推廣業(yè)務服務落地驗證
-
Go 語言優(yōu)化
- 適應范圍最廣,Go 服務都有收益
- 優(yōu)化方式
- 優(yōu)化內存分配策略
- 優(yōu)化代碼編譯流程,生成更高效的程序
- 內部壓測驗證
- 推廣業(yè)務服務落地驗證
參考資料
熟悉 Go 語言基礎后的必讀內容,go.dev/doc/effecti…
Dave Cheney 關于 Go 語言編程實踐的演講記錄,dave.cheney.net/practical-g…
《編程的原則:改善代碼質量的101個方法》,總結了很多編程原則,按照是什么 -> 為什么 -> 怎么做進行了說明,mp.weixin.qq.com/s/vXSZOl2Gt…
如何編寫整潔的 Go 代碼,github.com/Pungyeon/cl…
Go 官方博客,有關于 Go 的最新進展,go.dev/blog/
Dave Cheney 關于 Go 語言編程高性能編程的介紹,dave.cheney.net/high-perfor…
Go 語言高性能編程,博主總結了 Go 編程的一些性能建議, geektutu.com/post/high-p…
Google 其他編程語言編碼規(guī)范,可以對照參考,zh-google-styleguide.readthedocs.io/en/latest/