Go 語言開發設計指北

友情提示:此篇文章大約須要閱讀 20分鐘33秒,不足之處請多指教,感謝你的閱讀。 訂閱本站
此文章首發於 Debug客棧 |https://www.debuginn.cngit

Go 語言是一種強類型、編譯型的語言,在開發過程當中,代碼規範是尤其重要的,一個小小的失誤可能會帶來嚴重的事故,擁有一個良好的 Go 語言開發習慣是尤其重要的,遵照開發規範便於維護、便於閱讀理解和增長系統的健壯性。github

如下是咱們項目組開發規範加上本身開發遇到的問題及補充,但願對你有所幫助:
注:咱們將如下約束分爲三個等級,分別是:【強制】【推薦】【參考】golang

Go 編碼相關

【強制】代碼風格規範遵循 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 相關

【推薦】統一使用:做爲前綴後綴分隔符,這裏能夠根據 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緩存的組合方案來緩解壓力,削減峯值:

使用這個方法須要具有這幾個條件:

  • cache 內容與用戶無關,key 狀態很少,屬於公共信息;
  • 該cache內容時效性較高,可是訪問量較大,有峯值流量。
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)
}

【參考】對於請求量高,實時性也高的內容,若是純粹使用緩存,當緩存失效瞬間,會致使大量請求穿透到後端服務,致使後端服務有雪崩危險:

如何兼顧扛峯值,保護後端系統,同時也能保持實時性呢?在這種場景下,能夠採用隨機更新法更新數據,方法以下:

  1. 正常請求從緩存中讀取,緩存失效則從後端服務獲取;
  2. 在請求中根據隨機機率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鏈接數不可設置太高,會致使代理鏈接數打滿致使不可用情況;

相關文章
相關標籤/搜索