友情提示:此篇文章大約須要閱讀 20分鐘33秒,不足之處請多指教,感謝你的閱讀。 訂閱本站
此文章首發於 Debug客棧 |https://www.debuginn.cngit
Go 語言是一種強類型、編譯型的語言,在開發過程當中,代碼規範是尤其重要的,一個小小的失誤可能會帶來嚴重的事故,擁有一個良好的 Go 語言開發習慣是尤其重要的,遵照開發規範便於維護、便於閱讀理解和增長系統的健壯性。github
如下是咱們項目組開發規範加上本身開發遇到的問題及補充,但願對你有所幫助:
注:咱們將如下約束分爲三個等級,分別是:【強制】、【推薦】、【參考】。golang
【強制】代碼風格規範遵循 go 官方標準:CodeReviewComments,請使用官方golint lint 進行風格靜態分析;正則表達式
【強制】代碼格式規範依照gofmt
,請安裝相關 IDE 插件,在保存代碼或者編譯時,自動將源碼經過gofmt
作格式化處理,保證團隊代碼格式一致(好比空格,遞進等)redis
【強制】業務處理代碼中不能開goroutine
,此舉會致使goroutine
數量不可控,容易引發系統雪崩,若是須要啓用goroutine
作異步處理,請在初始化時啓用固定數量goroutine
,經過channel
和業務處理代碼交互,初始化goroutine
的函數,原則上應該從main
函數入口處明確的調用:算法
func crond() { defer func() { if err := recover(); err != nil { // dump stack & log } }() // do something } func main() { // init system go crond() go crond2() // handlers }
【強制】異步開啓goroutine
的地方(如各類cronder
),須要在最頂層增長recover()
,捕捉panic
,避免個別cronder
出錯致使總體退出:sql
func globalCrond() { for _ := ticker.C { projectCrond() itemCrond() userCrond() } } func projectCrond() { defer func() { if err := recover(); err != nil { // 打日誌,並預警 } } // do }
【強制】當有併發讀寫map
的操做,必須加上讀寫鎖RWMutex
,不然go runtime
會由於併發讀寫報panic
,或者使用sync.Map
替代;數據庫
【強制】對於提供給外部使用的package
,返回函數裏必須帶上err
返回,而且保證在err == nil
狀況下,返回結果不爲nil
,好比:編程
resp, err := package1.GetUserInfo(xxxxx) // 在err == nil 狀況下,resp不能爲nil或者空值
【強制】當操做有多個層級的結構體時,基於防護性編程的原則,須要對每一個層級作空指針或者空數據判別,特別是在處理複雜的頁面結構時,如:json
type Section struct { Item *SectionItem Height int64 Width int64 } type SectionItem struct { Tag string Icon string ImageURL string ImageList []string Action *SectionAction } type SectionAction struct { Type string Path string Extra string } func getSectionActionPath(section *Section) (path string, img string, err error) { if section.Item == nil || section.Item.Action == nil { // 要作好足夠防護,避免由於空指針致使的panic err = fmt.Errorf("section item is invalid") return } path = section.Item.Action.Path img = section.Item.ImageURL // 對取數組的內容,也必定加上防護性判斷 if len(section.Item.ImageList) > 0 { img = section.Item.ImageList[0] } return }
【推薦】生命期在函數內的資源對象,若是函數邏輯較爲複雜,建議使用defer
進行回收:
func MakeProject() { conn := pool.Get() defer pool.Put(conn) // 業務邏輯 ... return }
對於生命期在函數內的對象,定義在函數內,將使用棧空間,減小gc
壓力:
func MakeProject() (project *Project){ project := &Project{} // 使用堆空間 var tempProject Project // 使用棧空間 return }
【強制】不能在循環里加defer
,特別是defer
執行回收資源操做時。由於defer
是函數結束時才能執行,並不是循環結束時執行,某些狀況下會致使資源(如鏈接資源)被大量佔用而程序異常:
// 反例: for { row, err := db.Query("SELECT ...") if err != nil { ... } defer row.Close() // 這個操做會致使循環裏積攢許多臨時資源沒法釋放 ... } // 正確的處理,能夠在循環結束時直接close資源,若是處理邏輯較複雜,能夠打包成函數: for { func () { row, err := db.Query("SELECT ...") if err != nil { ... } defer row.Close() ... }() }
【推薦】對於可預見容量的slice
或者map
,在make
初始化時,指定cap
大小,能夠大大下降內存損耗,如:
headList := make([]home.Sections, 0, len(srcHomeSection)/2) tailList := make([]home.Sections, 0, len(srcHomeSection)/2) dstHomeSection = make([]*home.Sections, 0, len(srcHomeSection)) …. if appendToHead { headList = append(headList, info) } else { tailList = append(tailList, info) } …. dstHomeSection = append(dstHomeSection, headList…) dstHomeSection = append(dstHomeSection, tailList…)
【推薦】邏輯操做中涉及到頻繁拼接字符串的代碼,請使用bytes.Buffer
替代。使用string
進行拼接會致使每次拼接都新增string
對象,增長GC
負擔:
// 正例: var buf bytes.Buffer for _, name := range userList { buf.WriteString(name) buf.WriteString(",") } return buf.String() // 反例: var result string for _, name := range userList { result += name + "," } return result
【強制】對於固定的正則表達式,能夠在全局變量初始化時完成預編譯,能夠有效加快匹配速度,不須要在每次函數請求中預編譯:
var wordReg = regexp.MustCompile("[w]+") func matchWord(word string) bool { return wordReg.MatchString(word) }
【推薦】JSON 解析時,遇到不肯定是什麼結構的字段,建議使用json.RawMessage
而不要用interface
,這樣能夠根據業務場景,作二次unmarshal
並且性能比interface
快不少;
【強制】鎖使用的粒度須要根據實際狀況進行把控,若是變量只讀,則無需加鎖;讀寫,則使用讀寫鎖sync.RWMutex
;
【強制】使用隨機數時(math/rand
),必需要作隨機初始化(rand.Seed
),不然產生出的隨機數是可預期的,在某些場合下會帶來安全問題。通常狀況下,使用math/rand
能夠知足業務需求,若是開發的是安全模塊,建議使用crypto/rand
,安全性更好;
【推薦】對性能要求很高的服務,或者對程序響應時間要求高的服務,應該避免開啓大量gouroutine
;
說明:官方雖然號稱goroutine
是廉價的,能夠大量開啓goroutine
,可是因爲goroutine
的調度並無實現優先級控制,使得一些關鍵性的goroutine
(如網絡/磁盤IO,控制全局資源的goroutine
)沒有及時獲得調度而拖慢了總體服務的響應時間,於是在系統設計時,若是對性能要求很高,應避免開啓大量goroutine
。
【強制】打點使用.來作分隔符,打點名稱須要包含業務名,模塊,函數,函數處理分支等,參考以下:
// 業務名.服務名.模塊.功能.方法 service.gateway.module.action.func
【強制】打點使用場景是監控系統的實時狀態,不適合存儲任何業務數據;
【強制】在打點個數太多時,展現時速度會變慢。建議單個服務打點的key不超過10000個,key
中單個維度不一樣值不超過 1000個(千萬不要用 user_id 來打點);
【推薦】若是展現的時候須要拿成百上千個key
的數據經過 Graphite
的聚合函數作聚合,最後獲得一個或幾個 key
。這種狀況下能夠在打點的時候就把這個要聚合的點聚合好,這樣展現的時候只要拿這幾個 key,對展現速度是巨大的提高。
【強制】日誌信息需帶上下文,其中logid
必須帶上,同一個請求打的日誌都需帶上logid
,這樣能夠根據logid
查找該次請求相關的信息;
【強制】對debug/notice/info
級別的日誌輸出,必須使用條件輸出或者使用佔位符方式,避免使用字符拼接方式:
log.Debug("get home page failed %s, id %d", err, id)
【強制】若是是解析json
出錯的日誌,須要將報錯err
及原內容一併輸出,以方便覈查緣由;
【推薦】對debug/notice/info
級別的日誌,在打印日誌時,默認不顯示調用位置(如/path/to/code.go:335)
說明:go
獲取調用棧信息是比較耗時的操做(runtime.Caller
),對於性能要求很高的服務,特別是大量調用的地方,應儘可能避免開發人員在使用該功能時,需知悉這個調用帶來的代價。
【推薦】統一使用:
做爲前綴後綴分隔符,這裏能夠根據 Redis
中間件 key proxy
怎麼解析分析 Key
進行自定義,便於基礎服務的數據可視化及問題排查;
【強制】避免使用 HMGET/HGETALL/HVALS/HKEYS/SMEMBERS
阻塞命令這類命令在value
較大時,對 Redis 的 CPU/帶寬消耗較高,容易致使響應過慢引起系統雪崩;
【強制】不可把 Redis 當成存儲,若有統計相關的需求,能夠考慮異步同步到數據庫進行統計,Redis 應該回歸緩存的本質;
【推薦】避免使用大 key,按經驗超過 10k 的 value,能夠壓縮(gzip/snappy
等算法)後存入內存,能夠減小內存使用,其次下降網絡消耗,提升響應速度:
value, err := c.RedisCache.GetGzip(key) …. c.RedisCache.SetExGzip(content, 60)
【推薦】Redis 的分佈式鎖,可使用:
lock: redis.Do("SET", lockKey, randint, "EX", expire, "NX") unlock: redis.GetAndDel(lockKey, randint) // redis暫不支持,能夠用lua腳本
【推薦】儘可能避免在邏輯循環代碼中調用 Redis,會產生流量放大效應,請求量較大時需採用其餘方法優化(好比靜態配置文件);
【推薦】key
儘可能離散讀寫,經過uid/imei/xid
等跟用戶/請求相關的後綴分攤到不一樣分片,避免分片負載不均衡;
【參考】當緩存量大,請求量較高,可能超出 Redis 承受範圍時,可充分利用本地緩存(localcache)+redis
緩存的組合方案來緩解壓力,削減峯值:
使用這個方法須要具有這幾個條件:
key := "demoid:3344" value := localcacche.Get(key) if value == "" { value = rediscache.Get(key) if value != "" { // 隨機緩存 1~5s,各個機器間錯開峯值,只要比 redis緩存短便可 localcache.SetEx(key, value, rand.Int63n(5)+1) } } if value == "" { .... // 從其餘系統或者數據庫獲取數據 appoint.GetValue() // 同時設置到redis及localcache中 rediscache.SetEx(key, content, 60) localcache.SetEx(key, content, rand.Int63n(5)+1) }
【參考】對於請求量高,實時性也高的內容,若是純粹使用緩存,當緩存失效瞬間,會致使大量請求穿透到後端服務,致使後端服務有雪崩危險:
如何兼顧扛峯值,保護後端系統,同時也能保持實時性呢?在這種場景下,能夠採用隨機更新法更新數據,方法以下:
這種作法能保證最低時效性,而且當訪問量越大,更新機率越高,使得內容實時性也越高。
若是結合上一條 localcache+rediscache
作一二級緩存,則能夠達到扛峯值同時保持實時性。
【強制】操做數據庫 sql 必須使用 stmt 格式,使用佔位符替代參數,禁止拼接 sql;
【強制】SQL語句查詢時,不得使用 SELECT (即形如 SELECT FROM tbl WHERE),必須明確的給出要查詢的列名,避免表新增字段後報錯;
【強制】對於線上業務 SQL,需保證命中索引,索引設計基於業務需求及字段區分度,通常可區分狀態不高的字段(如 status 等只有幾個狀態),不建議加到索引中;
【強制】在成熟的語言中,有實體類,數據訪問層(repository / dao
)和業務邏輯層(service
);在咱們的規範中存儲實體struct
放置於entities
包下;
【強制】對於聯合索引,需將區分度較大的字段放前面,區分度小放後面,查找時能夠減小被檢索數據量;
-- 字段區分度 item_id > project_id alter table xxx add index idx_item_project (item_id, project_id)
【強制】全部數據庫表必須有主鍵 id;
【強制】主鍵索引名爲 pk字段名; 惟一索引名爲 uk字段名; 普通索引名則爲 idx_字段名;
【強制】防止因字段類型不一樣形成的隱式轉換,致使索引失效,形成全表掃描問題;
【強制】業務上有惟一特性的字段,即便是多字段的組合,也必須建成惟一索引;
【強制】通常事務標準操做流程:
func TestTXExample(t *testing.T) { // 打開事務 tx, err := db.Beginx() if err != nil { log.Fatal("%v", err) return } // defer異常 needRollback := true defer func() { if r := recover(); r != nil { // 處理recover,避免由於panic,資源沒法釋放 log.Fatal("%v", r) needRollback = true } if needRollback { xlog.Cause("test.example.transaction.rollback").Fatal() tx.Rollback() } }() // 事務的邏輯 err = InsertLog(tx, GenTestData(1)[0]) if err != nil { log.Fatal("%v", err) return } // 提交事務 err = tx.Commit() if err != nil { log.Fatal("%v", err) return } needRollback = false return }
【強制】執行事務操做時,請確保SELECT ... FOR UPDATE
條件命中索引,使用行鎖,避免一個事務鎖全表的狀況;
【強制】禁止超過三個表的join
,須要join
的字段,數據類型必須一致,多表關聯查詢時,保證被關聯的字段有索引;
【強制】數據庫max_open
鏈接數不可設置太高,會致使代理鏈接數打滿致使不可用情況;