Uber Go 規範 -一-: 編碼和命名

Uber 是一家美國硅谷的科技公司,也是 Go 語言的早期 adopter。其開源了很多 golang 項目,諸如被 Gopher 圈熟知的 zap、jaeger 等。2018 年年末 Uber 將內部的 Go 風格規範 開源到 GitHub,經過一年的積累和更新,該規範已經初具規模,並受到廣大 Gopher 的關注,本文是對規範的整理。

https://github.com/xxjwxc/uber_go_guide_cn

1. 包名

當命名包時,請按下面規則命名:

1.1  最後一個詞不是包名

如果程序包名稱與導入路徑的最後一個元素不匹配,則必須使用導入別名。

import (
  "net/http"

  client "example.com/client-go"  
  trace "example.com/trace/v2" // 最後一個元素是v2
)

1.2 有衝突時

在所有其他情況下,除非導入之間有直接衝突,否則應避免導入別名。

// 不推薦寫法
import (
  "fmt"
  "os"

  nettrace "golang.net/x/trace" // 沒有衝突,不使用別名
)

// 有衝突時,再起別名
import (
  "fmt"
  "os"
  "runtime/trace"

  nettrace "golang.net/x/trace"
)

2. 函數名

我們遵循 Go 社區關於使用 MixedCaps 作爲函數名 的約定, 使用駝峯命名法,不要使用下劃線。舉例:MixedCaps 或者mixedCaps

有一個例外,爲了對相關的測試用例進行分組,函數名可能包含下劃線,如:TestMyFunction_WhatIsBeingTested

3. 變量和常量

3.1  未導出帶前綴

對於未導出的全局 (包內)varsconsts, 前面加上前綴_,用來明確表示它們是全局符號。錯誤類型的變量例外,錯誤類型變量應以err開頭。

1. 不推薦寫法

// 包內變量
const (
  defaultPort = 8080
  defaultUser = "user"
)

// bar.go

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

2. 推薦寫法

// 包內變量
const (
  _defaultPort = 8080
  _defaultUser = "user"
)

3.2 相似聲明放一組

Go 語言支持將相似的聲明放在一個組內。

1. 反面示例

// 導入包
import "a"
import "b"
// 常量
const a = 1
const b = 2

var a = 1
var b = 2

type Area float64
type Volume float64
// 不要將不相關的聲明放在一組。
type Operation int

const (
  Add Operation = iota + 1
  Subtract
  Multiply
  EnvVar = "MY_ENV"
)

2.  推薦用法

// 導入包
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
)
const EnvVar = "MY_ENV"

3.3 本地變量聲明

如果將變量明確設置爲某個值,則應使用短變量聲明形式 (:=)。

// 反面示例
var s = "foo"
// 推薦寫法
s := "foo"

但是,在某些情況下,var 使用關鍵字時默認值會更清晰。例如,聲明空切片。

a. 反面示例

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

b. 推薦寫法

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

3.4 縮小變量作用域

如果有可能,儘量縮小變量作用範圍。除非它與 減少嵌套的規則衝突。

// 反面示例
err := ioutil.WriteFile(name, data, 0644)
if err != nil {
 return err
}

// 推薦寫法
if err := ioutil.WriteFile(name, data, 0644); err != nil {
 return err
}

4. import 分組

導入應該分爲兩組:標準庫、其他庫, 用空行分開

4.1 反面示例

// 未用空行分割
import (
  "fmt" // 標準庫 
  "os" // 標準庫 
  "go.uber.org/atomic" // 其他庫 
  "golang.org/x/sync/errgroup" // 其他庫 
)

4.2 推薦用法

import (
  "fmt"
  "os"

  "go.uber.org/atomic" // 其他庫 
  "golang.org/x/sync/errgroup" // 其他庫 
)

4.defer 使用

使用 defer 釋放資源,諸如文件和鎖。

4.1 反面示例

p.Lock()
if p.count < 10 {
  p.Unlock()
  return p.count
}
p.count++
newCount := p.count
p.Unlock()
return newCount
// 當有多個 return 分支時,很容易遺忘 unlock

4.2  推薦寫法

p.Lock()
defer p.Unlock()

if p.count < 10 {
  return p.count
}

p.count++
return p.count

// 更可讀

5. Channel 的 size

Channel 的 size 要麼是 1,要麼是無緩衝的

1cfpGz

6. 枚舉從 1 開始

Go 中引入枚舉的標準方法是聲明一個自定義類型和一個使用了 iotaconst 組。由於變量的默認值爲 0,因此通常應以非零值開頭枚舉。

6.1 反面示例

type Operation int

const (
  Add Operation = iota
  Subtract
  Multiply
)
// Add=0, Subtract=1, Multiply=2

6.2 推薦寫法

type Operation int

const (
  Add Operation = iota + 1
  Subtract
  Multiply
)

// Add=1, Subtract=2, Multiply=3

7. 斷言使用

類型斷言 將會在檢測到不正確的類型時,以單一返回值形式時會拋 panic

// 反面示例
t := i.(string)

// 推薦寫法
t, ok := i.(string)
if !ok {
  // 優雅地處理錯誤
}

8. 不要使用panic

在生產環境中運行的代碼必須避免出現 panicpanic 是 級聯失敗 的主要根源 。如果發生錯誤,該函數必須返回錯誤,並允許調用方決定如何處理它。

8.1 反面示例

func run(args []string) {
  if len(args) == 0 {
    panic("an argument is required")
  }
  // ...
}

func main() {
  run(os.Args[1:])
}

8.2 推薦寫法

func run(args []string) error {
  if len(args) == 0 {
    return errors.New("an argument is required")
  }
  // ...
  return nil
}

func main() {
  if err := run(os.Args[1:]); err != nil {
    fmt.Fprintln(os.Stderr, err)
    os.Exit(1)
  }
}

9. 避免使用init()

儘可能避免使用init()。當init()是不可避免或可取的,代碼應先嚐試:

  1. 無論程序環境或調用如何,都要完全確定。

  2. 避免依賴於其他init()函數的順序或副作用。雖然init()順序是明確的,但代碼可以更改, 因此init()函數之間的關係可能會使代碼變得脆弱和容易出錯。

  3. 避免訪問或操作全局或環境狀態,如機器信息、環境變量、工作目錄、程序參數 / 輸入等。

  4. 避免I/O,包括文件系統、網絡和系統調用。

儘可能的作爲main()函數流程中調用的一個環節。

9.1 反面示例

type Config struct {
    // ...
}
var _config Config
func init() {
    // Bad: 基於當前目錄
    cwd, _ := os.Getwd()
    // Bad: I/O
    raw, _ := ioutil.ReadFile(
        path.Join(cwd, "config", "config.yaml"),
    )
    yaml.Unmarshal(raw, &_config)
}

9.2 推薦寫法

type Config struct {
    // ...
}
// 提供一個獲取配置的方法
func loadConfig() Config {
    cwd, err := os.Getwd()
    // handle err
    raw, err := ioutil.ReadFile(
        path.Join(cwd, "config", "config.yaml"),
    )
    // handle err
    var config Config
    yaml.Unmarshal(raw, &config)
    return config
}

10. 流程優化

10.1 減少嵌套

代碼應儘可能先處理錯誤情況、特殊情況並儘早返回,依次來減少嵌套。

a. 反面示例

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)
  }
}

b. 推薦寫法

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()
}

10.2 不必要的 else

// 反面示例
var a int
if b {
  a = 100
} else {
  a = 10
}

// 推薦寫法
a := 10
if b {
  a = 100
}

11. 字符串

11.1 使用 ``,避免轉義

// 反面示例
wantError := "unknown name:\"test\""

//推薦寫法
wantError := `unknown error:"test"`

11.2 格式化字符串

如果你在函數外聲明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)
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/Tt3bKnxDWrdPMQEzzNlkvQ