痛點:
- 工程剛開始非常整潔倍靡,隨著時間的流逝,逐漸變得不太好維護了..
- 多人開發(fā)同一工程時课舍,架構(gòu)層次不清晰塌西,重復造輪子?
- 接手了一個舊工程筝尾,如何快速理解架構(gòu)與設(shè)計捡需,從而快速上手做需求?
有規(guī)范的好處:
- 利于多人合作開發(fā)&理解同一模塊/工程筹淫。
- 降低團隊成員之間的代碼溝通成本站辉。
- 架構(gòu)&代碼規(guī)范明確,有效提高編碼效率损姜。
前言:
讀這本書的時饰剥,第一個想到的問題就是:“什么是整潔的代碼?”
書中列舉了各位程序員鼻祖的名言摧阅,我整理總結(jié)了下汰蓉,大概有下面幾條:
- 邏輯直截了當,令缺陷難以隱藏 棒卷。
- 減少依賴關(guān)系顾孽,便于維護祝钢。
- 合理分層,完善錯誤處理 若厚。
- 只做好一件事太颤。沒有重復代碼。
代碼是團隊溝通的一種方式
工作的溝通盹沈,不只是每天lark拉群或者開會交流,代碼也是我們很重要的溝通方式之一吃谣。
用代碼寫完需求乞封,只是萬里長征的第一步。我們要用代碼表達自己的設(shè)計思想岗憋。如果我們團隊大部分人都能按照一定規(guī)范肃晚、思路去寫代碼。那么仔戈,工作溝通成本會降低許多关串。
比如:某位同學之前負責的一個模塊,被另一位同事接手了监徘,或者隨著業(yè)務的擴張晋修,我們多個同學共同開發(fā)同一個工程/模塊。如果我們的代碼結(jié)構(gòu)大同小異凰盔,分層清晰墓卦、注釋合理,就會降低很多溝通成本户敬。
因此落剪,我們需要為團隊創(chuàng)造整潔的代碼。
一是降低團隊內(nèi)的代碼溝通成本尿庐,二是便于今后項目需求的維護與迭代忠怖。
讓營地比來時更整潔
隨著需求的不斷迭代,保持代碼整潔抄瑟、工程更易理解凡泣。
有時候,我們會維護一些老項目锐借,或者交接過來的項目问麸。代碼可能不太美觀,工程可能不太好理解钞翔。
一般我們會面臨兩種選擇:
- 重構(gòu)
- 優(yōu)化迭代
重構(gòu)的成本比較高严卖,得先理解原有邏輯,再進行重新設(shè)計落地布轿。代價大哮笆,周期長来颤,短期看不到效果。
在人力有限的情況下稠肘。我們一般會先選擇“優(yōu)化迭代”福铅。
這時候,我們每做一個新需求 / 修復一個bug時项阴,我們要盡可能的去小范圍“重構(gòu)”滑黔。
每一次Merge
,代碼都比之前更干凈环揽,工程變得更好理解略荡。那么,我們的工程就不會變的更糟歉胶。
清理不一定要花多少功夫汛兜。也許只是改一個更加容易理解的命名;抽象一個函數(shù)通今,消除一點重復/冗余代碼粥谬;處理一下嵌套的 if / else 等等。
一辫塌、有意義的命名
名副其實:
起有意義的名字漏策,讓人一目了然。
一看這個變量璃氢,就能知道它存儲的是什么對象哟玷。
一看這個方法,就能知道它處理的是什么事一也。
一看這個包名巢寡,就能知道它負責處理哪個模塊。
看看反例:
var array []int64
var theList []int64
var num int64
看看正例:
var mrList []*MRInfo
var buildNum int64
避免誤導:
不要用太長或者很偏僻的單詞來命名椰苟,也不要用拼音代替英文抑月。
更不要用容易混淆的字母(字母+數(shù)字)。尤其是l
和O
兩個字母舆蝴,和數(shù)字1和0太像了谦絮。
看看反例:
func getDiZhi() string {
// ..
}
func modifyPassword(password1, password2 string) string {
// ..
}
看看正例:
func getAddress() string {
// ..
}
func modifyPassword(oldPassword, newPassword string) string {
// ..
}
有意義的區(qū)分:
聲明兩個同類型的變量/函數(shù),需要用有明確意義的命名加以區(qū)分洁仗。
看看反例:
var accountData []*Account
var account []*Account
func Account(id int) *Account {
// ...
}
func AccountData(id int) *Account {
// ...
}
可讀可搜索:
起可讀的层皱,可以被搜索的名字。
看看反例:
var ymdhms = "2021-08-04 01:55:55"
var a = 1
看看正例:
var date = "2021-08-04 01:55:55"
var buildNum = 1
命名規(guī)范(重點)
package
- 同一項目下赠潦,不允許出現(xiàn)同名的package叫胖。
- 只由小寫字母組成。不包含大寫字母和下劃線等字符她奥。
- 簡短并包含一定的上下文信息瓮增。例如
time
怎棱、http
等。 - 不能是含義模糊的常用名绷跑,或者與標準庫同名拳恋。例如不能使用
util
或者strings
。 - 包名能夠作為路徑的 base name砸捏,在一些必要的時候谬运,需要把不同功能拆分為子包。(例如應該使用
encoding/base64
而不是encoding_base64
或者encodingbase64
垦藏。)
以下規(guī)則按照先后順序盡量滿足:
- 不使用常用變量名作為包名吩谦。
- 使用單數(shù)而不是復數(shù)。(關(guān)鍵字除外膝藕,例如
consts
) - 謹慎地使用縮寫,保證理解咐扭。
文件名
- 文件名都使用小寫字母芭挽,且使用單數(shù)形式,如需要可使用下劃線分割蝗肪。
函數(shù)和方法
Function 的命名應該遵循如下原則:
- 對于可導出的函數(shù)使用大寫字母開頭袜爪,對于內(nèi)部使用的函數(shù)使用小寫字母開頭。
- 若函數(shù)或方法為判斷類型(返回值主要為 bool 類型)薛闪,則名稱應以 has, is, can 等判斷性動詞開頭辛馆。
// HasPrefix tests whether the string s begins with prefix.
func HasPrefix(s, prefix string) bool {...
- 函數(shù)采用駝峰命名,不能使用下劃線豁延,不能重復包名前綴昙篙。例如使用
http.Server
而不是http.HTTPServer
,因為包名和函數(shù)名總是成對出現(xiàn)的诱咏。
// WriteRune appends the UTF-8 encoding of Unicode code point r to b's buffer.
// It returns the length of r and a nil error.
func (b *Builder) WriteRune(r rune) (int, error) {...
- 遵守簡單的原則苔可,不應該像 ToString 這類的方法名,而直接使用 String 代替袋狞。
// String returns the accumulated string.
func (b *Builder) String() string {...
- Receiver 要盡量簡短并有意義
- 不要使用面向?qū)ο缶幊讨械某S妹俑ā@绮灰褂?code>self、
this
苟鸯、me
等同蜻。 - 一般使用 1 到 2 個字母的縮寫代表其原來的類型。例如類型為
Client
早处,可以使用c
湾蔓、cl
等。 - 在每個此類型的方法中使用統(tǒng)一的縮寫陕赃。例如在其中一個方法中使用了
c
代表了Client
卵蛉,在其他的方法中也要使用c
而不能使用諸如cl
的命名颁股。
- 不要使用面向?qū)ο缶幊讨械某S妹俑ā@绮灰褂?code>self、
func (r *Reader) Len() int {...
常量
- 常量使用駝峰形式。(盡量不要用下劃線)
const AppVersion = "1.1.1"
- 如果是枚舉類型的常量傻丝,需要先創(chuàng)建相應類型:
type Scheme string
const (
HTTP Scheme = "http"
HTTPS Scheme = "https"
)
變量
- 變量命名基本上遵循相應的英文表達或簡寫甘有。
- 采用駝峰命名,不能使用下劃線葡缰。首字母是否大寫根據(jù)是否需要外部訪問來定亏掀。
- 遇到專有名詞時,可以不改變原來的寫法泛释。例如:
{
"API": true,
"ASCII": true,
"CPU": true,
"CSS": true,
"DNS": true,
"EOF": true,
"GUID": true,
"HTML": true,
"HTTP": true,
"HTTPS": true,
"ID": true,
"IP": true,
"JSON": true,
"LHS": true,
"QPS": true,
"RAM": true,
"RHS": true,
"RPC": true,
"SLA": true,
"SMTP": true,
"SSH": true,
"TLS": true,
"TTL": true,
"UI": true,
"UID": true,
"UUID": true,
"URI": true,
"URL": true,
"UTF8": true,
"VM": true,
"XML": true,
"XSRF": true,
"XSS": true,
}
二滤愕、函數(shù)
短小
盡可能的縮短每個函數(shù)的長度。能抽象就抽象怜校。
任何一個函數(shù)都不應該超過50
行间影。甚至,20
行封頂最佳茄茁。(PS:16寸mac滿屏是60多行)
想象下魂贬,如果有個幾百行,甚至上千行的函數(shù)裙顽。后面維護得多困難付燥。
單參數(shù)
每個函數(shù)最理想應該是有0或1個入?yún)ⅰ?br> 盡量不要超過三個入?yún)ⅰH绻^愈犹,建議封裝成結(jié)構(gòu)體键科。
只做一件事
函數(shù)應該只做一件事,做好這件事漩怎,只做這一件事勋颖。
抽象層級
按順序,自頂向下讀代碼/寫代碼勋锤。
看看反例:
// 更新組件升級結(jié)果
func UpdatePodUpgradeResult(ctx context.Context, req *UpdatePodReq) error {
// 更新組件核心表牙言,寫了20行
// 更新歷史,寫了40行
// 更新構(gòu)建產(chǎn)物怪得,寫了20行
// ...代碼越來越多咱枉,越來越不好維護。
return nil
}
看看正例:
// 更新組件升級結(jié)果
func UpdatePodUpgradeResult(ctx context.Context, req *UpdatePodReq) error {
// 更新組件
err = updatePodMain(ctx, req)
if err != nil {
return err
}
// 更新歷史
err = updatePodHistory(ctx, req)
if err != nil {
return err
}
// 更新Builds
err = updatePodBuilds(ctx, req)
if err != nil {
return err
}
return nil
}
func updatePodMain(ctx context.Context, req *UpdatePodReq) error {
// ...
}
func updatePodHistory(ctx context.Context, req *UpdatePodReq) error {
// ...
}
func updatePodBuilds(ctx context.Context, req *UpdatePodReq) error {
// ...
}
盡量少嵌套 if / else
看看反例:
func GetItem(extension string) (Item, error) {
if refIface, ok := db.ReferenceCache.Get(extension); ok {
if ref, ok := refIface.(string); ok {
if itemIface, ok := db.ItemCache.Get(ref); ok {
if item, ok := itemIface.(Item); ok {
if item.Active {
return Item, nil
} else {
return EmptyItem, errors.New("no active item found in cache")
}
} else {
return EmptyItem, errors.New("could not cast cache interface to Item")
}
} else {
return EmptyItem, errors.New("extension was not found in cache reference")
}
} else {
return EmptyItem, errors.New("could not cast cache reference interface to Item")
}
}
return EmptyItem, errors.New("reference not found in cache")
}
看看正例:
func GetItem(extension string) (Item, error) {
refIface, ok := db.ReferenceCache.Get(extension)
if !ok {
return EmptyItem, errors.New("reference not found in cache")
}
ref, ok := refIface.(string)
if !ok {
// return cast error on reference
}
itemIface, ok := db.ItemCache.Get(ref)
if !ok {
// return no item found in cache by reference
}
item, ok := itemIface.(Item)
if !ok {
// return cast error on item interface
}
if !item.Active {
// return no item active
}
return Item, nil
}
安全并發(fā)處理(SafeGo)
建議:開協(xié)程的地方徒恋,盡量使用SafeGo(內(nèi)部有 recover 以及打印 panic 堆棧日志)
func SafeGo(ctx context.Context, f func()) {
go func() {
defer func() {
if err := recover(); err != nil {
content := fmt.Sprintf("Safe Go Capture Panic In Go Groutine\n%s", string(debug.Stack())){
logs.CtxFatal(ctx, content)
}
}
}()
f()
}()
}
For 循環(huán)并發(fā)處理(Routine Pool)
for 循環(huán)開協(xié)程時蚕断,優(yōu)先考慮使用封裝的
Routine Pool
(協(xié)程池)控制并發(fā)量。
好處:
- 避免協(xié)程創(chuàng)建過多入挣,導致程序崩潰亿乳。(對服務本身)
- 控制流量速度,防止把下游服務打雪崩。(對下游服務)
參考代碼:
type content struct {
work func() error
end *struct{}
}
func work(w func() error) content {
return content{work: w}
}
func end() content {
return content{end: &struct{}{}}
}
// Goroutine routine_pool
type RoutinePool struct {
capacity uint
ch chan content
}
func NewRoutinePool(ctx context.Context, capacity uint) *RoutinePool {
ch := make(chan content)
pool := RoutinePool{
capacity: capacity,
ch: ch,
}
for i := uint(0); i < capacity; i++ {
SafeGo(ctx, func() {
for {
select {
case cont := <-ch:
if cont.end != nil {
return
}
if cont.work != nil {
if err := cont.work(); err != nil {
LogCtxError(ctx, "run work failed: %v", err)
}
}
}
}
})
}
return &pool
}
func (pool *RoutinePool) Submit(w func() error) {
pool.ch <- work(w)
}
func (pool *RoutinePool) Shutdown() {
defer close(pool.ch)
for i := uint(0); i < pool.capacity; i++ {
pool.ch <- end()
}
}
Copy 傳入?yún)f(xié)程的 Context
Gin:直接調(diào)用
context.Copy()
即可葛假。
三障陶、注釋與格式
注釋
- 所有可導出的函數(shù)、類型聊训、變量等都應該有注釋抱究,注釋以函數(shù)名、類型名带斑、變量名打頭鼓寺,函數(shù)注釋建議同時包含參數(shù)和返回值的說明。
- 每行注釋不超過100個字符勋磕。
- 包妈候、函數(shù)、方法和類型的注釋說明都是一個完整的句子挂滓。
- 有具體方案文檔苦银,在對應地方留下文檔鏈接注釋。便于后續(xù)快速了解這部分需求赶站。
格式
這部分只要我們打開 Goland 相關(guān)配置墓毒,即可完成。
推薦配置
File Watcher 開啟 go fmt亲怠、go imports:
配置可以參考:https://www.jetbrains.com/help/go/using-file-watchers.html#enableFileWatcher
垂直格式:
每個文件從上到下的代碼規(guī)范。
一個文件柠辞,盡量不要超過 400 行团秽。(超過可讀性會降低)
- 垂直方向的間隔:
package聲明、導入聲明和每個函數(shù)之間都要有一個空行隔開叭首。
- 垂直方向的靠近:
靠的越近的代碼习勤,關(guān)系越緊密。
- 垂直距離:
變量聲明:盡可能靠近其使用的位置焙格。
局部變量图毕,聲明在函數(shù)頂部。
實體變量眷唉,聲明在類的頂部予颤。
相關(guān)函數(shù):盡節(jié)能互相靠近,保證順序冬阳。
首先蛤虐,應該放到一起。
其次肝陪,“調(diào)用”函數(shù)應該放到“被調(diào)用”函數(shù)的上面驳庭。
概念相關(guān):做某類事情的函數(shù),應該放一起。
比如饲常,一個 interface蹲堂,它有 read/write 方法,他們應該放一起
- 垂直順序:
“調(diào)用”函數(shù)應該放到“被調(diào)用”函數(shù)的上面贝淤。
建立了一種自頂向下貫穿源代碼的良好信息流柒竞。
橫向格式:
每一行代碼從左到右的代碼規(guī)范。
每一行代碼霹娄,盡量不要超過 120 個字能犯。(超過150字,一個屏幕就看不全了)
- 水平方向的間隔與靠近
操作符周圍加上空格犬耻。
- 水平對齊
type PodType string
const (
PodTypeIOS PodType = "iOS"
PodTypeAndroid PodType = "Android"
PodTypeFlutter PodType = "Flutter"
)
- 縮進
這部分 go-fmt 幫我們做了踩晶,只要集成 go-fmt 即可。
四枕磁、對象與數(shù)據(jù)結(jié)構(gòu)
數(shù)據(jù)抽象成對象
以組件升級為例渡蜻,將組件升級流程抽象成對象。不關(guān)心底層的數(shù)據(jù)結(jié)構(gòu)與實現(xiàn)计济。
分析茸苇,組件升級流程需要:
- ValidateParam(校驗參數(shù))
- FormatParam(格式化參數(shù))
- SendUpgradeRequest(觸發(fā)升級)
- GenerateHistory(生成歷史)
- UpdateHistory(更新歷史)
type mpaasRepoUpgradeHandlerType interface {
ValidateParam(ctx context.Context) error //判斷某個升級請求,是否合法
FormatUpgradeParam(ctx context.Context) error //處理參數(shù)沦寂,補充額外信息或者補上默認信息等等
SendUpgradeRequest(ctx context.Context, history *podHistory) (int, error) //各 Handler 自行發(fā)送升級請求
UpgradeHistory(ctx context.Context) *podHistory //生成升級歷史
UpdateHistoryInfo(ctx context.Context) *podHistory //重試的時候要更新的組件升級歷史字段
baseHandler() *podUpgradeBaseHandler //獲取 baseHandler
}
組件升級會分為多種:iOS
学密、Android
、Flutter
传藏、Custom
(構(gòu)建腳本)腻暮、RubyGem
等等..
不論哪種組件升級只要實現(xiàn)這套 interface,即可完成組件升級流程毯侦。
數(shù)據(jù) vs. 對象
對象:把數(shù)據(jù)隱藏于抽象之后哭靖,暴露操作數(shù)據(jù)的方法。
數(shù)據(jù):通過數(shù)據(jù)結(jié)構(gòu)暴露處理侈离。
面向過程(直接使用數(shù)據(jù)結(jié)構(gòu)):
好處:在不改動既有數(shù)據(jù)結(jié)構(gòu)的前提下试幽,新增新函數(shù)。
壞處:難以增刪改數(shù)據(jù)結(jié)構(gòu)卦碾。
面向?qū)ο螅ǔ橄螅?br> 好處:方便增刪改數(shù)據(jù)結(jié)構(gòu)铺坞。
壞處:難以新增函數(shù),必須所有類改洲胖。
兩者沒有絕對的優(yōu)劣比較康震,需要 case by case 在具體場景下的應用。
得墨忒(tuī)耳律
模塊不應該了解它所操作對象的內(nèi)部結(jié)構(gòu)宾濒。
對象需要隱藏數(shù)據(jù)腿短,暴露操作。
五、錯誤處理
常規(guī)流程
- 先看看反例:
package smelly
func (store *Store) GetItem(id string) (Item, error) {
store.mtx.Lock()
defer store.mtx.Unlock()
item, ok := store.items[id]
if !ok {
return Item{}, errors.New("item could not be found in the store")
}
return item, nil
}
handler
里如果要對特殊錯誤做特殊處理:
func GetItemHandler(w http.ReponseWriter, r http.Request) {
item, err := smelly.GetItem("123")
if err != nil {
if err.Error() == "item could not be found in the store" {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
http.Error(w, errr.Error(), http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(item)
}
- 再看看正例:
提前在包里橘忱,定義好錯誤類型赴魁。
package clean
var (
ErrItemNotFound = errors.New("item could not be found in the store")
)
func (store *Store) GetItem(id string) (Item, error) {
store.mtx.Lock()
defer store.mtx.Unlock()
item, ok := store.items[id]
if !ok {
return nil, ErrItemNotFound
}
return item, nil
}
handler
里如果要對特殊錯誤做特殊處理:
func GetItemHandler(w http.ReponseWriter, r http.Request) {
item, err := clean.GetItem("123")
if err != nil {
if errors.Is(err, clean.ErrItemNotFound) {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(item)
}
好處:方便拓展,增加代碼可讀性钝诚。
六颖御、邊界
我們的系統(tǒng)都微服務化了。
每個子服務都會存在自己的邊界凝颇。
我們需要盡量保證我們的服務邊界整潔潘拱。
邊界整潔
我們依賴的服務、庫拧略、代碼是要可控的芦岂。
假如,我們依賴了一個不可控的庫垫蛆。
如果他有一天被檢測出有安全問題禽最、亦或 bug。
我們就很被動袱饭,導致服務需要大改川无。
簡單來說,依賴我們能控制的東西虑乖,好過依賴我們控制不了的東西懦趋。
免得日后被控制,導致重寫或修改疹味。
層級架構(gòu)明確
屬于同一層的服務仅叫,最好只依賴下層服務。
理論上來說佛猛,不該依賴同層服務,更不應該依賴上層服務坠狡。
每個團隊/業(yè)務的架構(gòu)圖應該要梳理出來继找。
模塊職責明確
其實,不光服務于服務之間要有層級架構(gòu)逃沿。
我們服務內(nèi)部應該也需要按照層級來寫代碼婴渡。
另外,每個工程的 ReadMe凯亮,最好能闡述下大概設(shè)計思路和架構(gòu)边臼,便于協(xié)作開發(fā)。