uber go code 規範(規範)

前言

從接觸 Golang 到現在, 感覺到的很深的一點是, go 的代碼無論是大佬還是菜鳥寫出的代碼, 都有着大體統一的 格式/流程, 這也是 Go 被開發者喜愛的一個原因, 但是還有一些, 比如變量的命名方式等, 可以稱之爲 風格 的東西, 卻不盡相同, 我在開發中, 其實也希望有一個相對權威的指導意見, 後來就找到了 uber 團隊出品的開發規範.
uber 是衆多公司中, 比較早使用 go 語言的了, 其本身也開源了一些優質的模塊, 有機會的話希望也能向大家展示一下, 而在 uber 內部開發中, 經過持續的迭代, 開源了自己的代碼規範, 這裏給大家解讀一下
需要特別指出的是, 下面的內容並不是一定需要遵守, 這裏你可以選擇自己認爲正確的可行的規範.
團隊內使用統一的風格, 可以提高代碼的可讀性
本篇記錄規範部分
推薦大家使用VsCode的官方golang插件

避免過長的行

避免需要讀者水平滾動或者過度轉動頭部的代碼行.
建議將行長度限制爲99字符以內, 注意只是建議.

將相似的聲明放置在一組

聲明指的是 導入/常量定義/變量定義等
將其分組可以提高可讀性, 分組依據是作用或者意思相近
錯誤示例

import "a"
import "b"


const a = 1
const b = 2

var a = 1
var b = 2

type Area float64
type Volume float64

正確示例

import (
  "a"
  "b"
)

const (
  a = 1
  b = 2
)

var (
  a = 1
  b = 2
)

type (
  Area float64
  Volume float64
)

需要注意不要強行放在一組, 如果是不相干的聲明要分開, 不要強行擠在一起
錯誤示例

type Operation int

// 強行擠到一起
const (
  Add Operation = iota + 1
  Subtract
  Multiply
  EnvVar = "MY_ENV"
)

正確示例

type Operation int

const (
  Add Operation = iota + 1
  Subtract
  Multiply
)

const EnvVar = "MY_ENV"

函數的內部也可以進行分組

func f() string {
  var (
    red   = color.New(0xff0000)
    green = color.New(0x00ff00)
    blue  = color.New(0x0000ff)
  )

  ...
}

對於函數內部的變量分組, 如果是語義不達到分組標準但是與其他變量相近, 也可以放到一起

func (c *client) request() {
  var (
    caller  = c.name
    format  = "json"
    timeout = 5*time.Second
    err error
  )
  // ...
}

import 分組

import也需要分組, 中間以空行分隔, 可分爲三組

  • 標準庫
  • 引入的第三方庫
  • 本項目的其他代碼包
    使用VsCode也可以自己自定義分組, 注意, 默認時VsCode只能分出標準和非標準, 也就是說第三方庫和本項目其他代碼擠在一起, 想要開啓區分, 需要添加VsCode配置
// 項目文件夾/.vscode/settings.json
{
    "gopls": {
        "formatting.local": "server-api"  // 本項目包名 
    }
}

包名

包名規則符合以下幾點

函數名

使用 MixedCaps 命名法(駝峯), 不要包含下劃線.
有一個例外, 測試代碼可以包含下劃線, 來區分測試函數的多個用例, 例如: TestMyFunction_WhatIsBeingTested

導入別名

如果包名稱與導入的路徑的最後一個名字不匹配則必須使用導入別名, VsCode會自己處理

import (
  "net/http"

  client "example.com/client-go"
  trace "example.com/trace/v2"
)

其他情況下, 除非導入有衝突, 不然不要使用別名
錯誤示例

import (
  "fmt"
  "os"

  nettrace "golang.net/x/trace"
)

正確示例

import (
  "fmt"
  "os"
  "runtime/trace"

  nettrace "golang.net/x/trace"
)

函數的分組和順序

  • 函數應該按照粗略的調用順序排序
  • 同一個文件中的函數應該按照調用者進行分組
    公開的函數應該先出現在文件中, 只放在 struct/const/var 的後面
    常用的場景是定義一個結構體並對外暴露方法, 此時結構體的初始化應該放置在方法前
    普通的工具函數放置在文件末尾
    錯誤示例
func (s *something) Cost() {
  return calcCost(s.weights)
}

type something struct{ ... }

func calcCost(n []int) int {...}

func (s *something) Stop() {...}

func newSomething() *something {
    return &something{}
}

正確示例

type something struct{ ... }

func newSomething() *something {
    return &something{}
}

func (s *something) Cost() {
  return calcCost(s.weights)
}

func (s *something) Stop() {...}

func calcCost(n []int) int {...}

減少嵌套

函數應該優先處理錯誤和特殊情況, 減少錯誤時代碼嵌套, 這樣有利於代碼可讀性
錯誤示例

for _, v := range data {
  if v.F1 == 1 {
    v = process(v)
    if err := v.Call(); err == nil {
      v.Send()
    } else {
      return err
    }
  } else {
    log.Printf("Invalid v: %v", v)
  }
}

正確示例

for _, v := range data {
  if v.F1 != 1 {
    log.Printf("Invalid v: %v", v)
    continue
  }

  v = process(v)
  if err := v.Call(); err != nil {
    return err
  }
  v.Send()
}

不必要的else

如果在 if 的兩個分支中都設置了變量, 則可以將其替換爲單個 if
錯誤示例

var a int
if b {
  a = 100
} else {
  a = 10
}

正確示例

a := 10
if b {
  a = 100
}

頂層的變量聲明

在文件頂層, 使用標準的 var 時, 可以不用指定類型, 除非他和表達式返回的類型不一樣
錯誤示例

var _s string = F()  // 我知道他是string

func F() string { return "A" }

正確示例

var _s = F()
// 由於 F 已經明確了返回一個字符串類型,因此我們沒有必要顯式指定_s 的類型
// 還是那種類型

func F() string { return "A" }

私有的頂層變量和常量使用_作爲前綴

頂級變量和常量作用域通常在包內, 並且名稱都是通用的, 很容器在其他文件中被意外使用
錯誤示例

// foo.go

const (
  defaultPort = 8080
  defaultUser = "user"
)

// bar.go

func Bar() {
  defaultPort := 9090
  ...
  fmt.Println("Default port", defaultPort)  // 9090
}

正確示例

// foo.go

const (
  _defaultPort = 8080
  _defaultUser = "user"
)

結構體的嵌入

對於結構體嵌套, 應該將嵌套的結構體放置在最上方, 並且有空行分隔
錯誤示例

type Client struct {
  version int
  http.Client
}

正確示例

type Client struct {
  http.Client

  version int
}

需要注意的是, 儘量不要使用匿名的方式來嵌套結構體, 嵌入不應該:

  • 純粹是爲了美觀或方便。
  • 使外部類型更難構造或使用。
  • 影響外部類型的零值。如果外部類型有一個有用的零值,則在嵌入內部類型之後應該仍然有一個有用的零值。
  • 作爲嵌入內部類型的副作用,從外部類型公開不相關的函數或字段。
  • 公開未導出的類型。
  • 影響外部類型的複製形式。
  • 更改外部類型的 API 或類型語義。
  • 嵌入內部類型的非規範形式。
  • 公開外部類型的實現詳細信息。
  • 允許用戶觀察或控制類型內部。
  • 通過包裝的方式改變內部函數的一般行爲,這種包裝方式會給用戶帶來一些意料之外情況。

本地變量聲明

將變量明確的設置爲某個值, 應該使用:=
錯誤示例

var s = "foo"

正確示例

s := "foo"

而在有時候, 使用 var 會更加清晰, 例如 聲明一個空切片
錯誤示例

func f(list []int) {
  filtered := []int{}
  for _, v := range list {
    if v > 10 {
      filtered = append(filtered, v)
    }
  }
}

正確示例

func f(list []int) {
  var filtered []int
  for _, v := range list {
    if v > 10 {
      filtered = append(filtered, v)
    }
  }
}

切片爲nil也是有效的

nil其實是一個有效的切片, 只是他的長度爲0, 因此

如果你需要返回一個長度爲0的切片, 建議可以直接返回 nil
錯誤示例

if x == "" {
  return []int{}
}

正確示例

if x == "" {
  return nil
}

如果你需要檢查切片是否爲空, 請使用len()
錯誤示例

func isEmpty(s []string) bool {
  return s == nil
}

正確示例

func isEmpty(s []string) bool {
  return len(s) == 0
}

var 創建的切片可以直接使用, 不需要make
錯誤示例

nums := []int{}
// or, nums := make([]int)

if add1 {
  nums = append(nums, 1)
}

if add2 {
  nums = append(nums, 2)
}

正確示例

var nums []int

if add1 {
  nums = append(nums, 1)
}

if add2 {
  nums = append(nums, 2)
}

雖然nil是一個有效的切片, 但是並不完全等於一個長度爲0的切片, 比如在序列化時會有不同的處理方式.

縮小變量的作用域

儘量縮小變量的作用域
錯誤示例

err := os.WriteFile(name, data, 0644)
if err != nil {
 return err
}

正確示例

if err := os.WriteFile(name, data, 0644); err != nil {
 return err
}

當然這個規則優先級要比 優先處理錯誤 的規則低
錯誤示例

if data, err := os.ReadFile(name); err == nil {
  err = cfg.Decode(data)
  if err != nil {
    return err
  }

  fmt.Println(cfg)
  return nil
} else {
  return err
}

正確示例

data, err := os.ReadFile(name)
if err != nil {
   return err
}

if err := cfg.Decode(data); err != nil {
  return err
}

fmt.Println(cfg)
return nil

避免參數語義不明確

參數的命名要保證可讀性, 如果參數名無法很好的表達語義, 可以添加C樣式的註釋
錯誤示例

func printInfo(name string, isLocal, done bool){
  ....
}

printInfo("foo", true, true)

正確示例

type Region int  // 多種狀態

const (
  UnknownRegion Region = iota
  Local
)

type Status int  // 多種狀態

const (
  StatusReady Status= iota + 1
  StatusDone
)

func printInfo(name string, region Region, status Status)

使用原始字符串字面值

golang可以使用 " ` " 來表示原始字符串
錯誤示例

wantError := "unknown name:\"test\""

正確示例

wantError := `unknown error:"test"`

結構體初始化

使用字段名而不是順序來初始化

保證可讀性, 同時也保證了結構體字段變化時兼容
錯誤示例

k := User{"John", "Doe", true}

正確示例

k := User{
    FirstName: "John",
    LastName: "Doe",
    Admin: true,
}

而當結構體只有2個或以下字段也可以使用順序的方式

省略結構中的零值

如果結構體中的某些字段現在是零值, 那麼不需要顯式的賦值, 可以直接忽略
錯誤示例

user := User{
  FirstName: "John",
  LastName: "Doe",
  MiddleName: "",
  Admin: false,
}

正確示例

user := User{
  FirstName: "John",
  LastName: "Doe",
}

對零值結構直接使用var

如果在聲明中省略了結構的所有字段,請使用 var 聲明結構
錯誤示例

user := User{}

正確示例

var user User

初始化結構體引用

請使用&T{}代替new(T),以使其與結構體初始化一致.
錯誤示例

sval := T{Name: "foo"}

// inconsistent
sptr := new(T)
sptr.Name = "bar"

正確示例

sval := T{Name: "foo"}

sptr := &T{Name: "bar"}

初始化maps

對於空 map 請使用 make() 初始化, 並且 map 是通過編程方式填充的。 這使得 map 初始化在表現上不同於聲明,並且它還可以方便地在 make 後添加大小提示
錯誤示例

var (
  // m1 讀寫安全;
  // m2 在寫入時會 panic
  m1 = map[T1]T2{}
  m2 map[T1]T2
)

正確示例

var (
  // m1 讀寫安全;
  // m2 在寫入時會 panic
  m1 = make(map[T1]T2)
  m2 map[T1]T2
)

字符串format模板

如果你在函數外聲明Printf-style 函數的格式字符串,請將其設置爲const常量
這有助於go vet對格式字符串執行靜態分析
正確示例

msg := "unexpected values %v, %v\n"
fmt.Printf(msg, 1, 2)

錯誤示例

const msg = "unexpected values %v, %v\n"
fmt.Printf(msg, 1, 2)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章