主頁 >  其他 > Uber Go 語言編碼規范

Uber Go 語言編碼規范

2020-09-16 08:01:31 其他

Uber Go 語言編碼規范

Uber 是一家美國硅谷的科技公司,也是 Go 語言的早期 adopter,其開源了很多 golang 專案,諸如被 Gopher 圈熟知的 zap、jaeger 等,2018 年年末 Uber 將內部的 Go 風格規范 開源到 GitHub,經過一年的積累和更新,該規范已經初具規模,并受到廣大 Gopher 的關注,本文是該規范的中文版本,本版本會根據原版實時更新,

版本

  • 當前更新版本:2019-11-13 版本地址:commit:#71
  • 如果您發現任何更新、問題或改進,請隨時 fork 和 PR
  • Please feel free to fork and PR if you find any updates, issues or improvement.

目錄

  • 介紹
  • 指導原則
    • 指向 interface 的指標
    • 接收器 (receiver) 與介面
    • 零值 Mutex 是有效的
    • 在邊界處拷貝 Slices 和 Maps
    • 使用 defer 釋放資源
    • Channel 的 size 要么是 1,要么是無緩沖的
    • 列舉從 1 開始
    • 錯誤型別
    • 錯誤包裝 (Error Wrapping)
    • 處理型別斷言失敗
    • 不要 panic
    • 使用 go.uber.org/atomic
  • 性能
    • 優先使用 strconv 而不是 fmt
    • 避免字串到位元組的轉換
    • 盡量初始化時指定 Map 容量
  • 規范
    • 一致性
    • 相似的宣告放在一組
    • import 分組
    • 包名
    • 函式名
    • 匯入別名
    • 函式分組與順序
    • 減少嵌套
    • 不必要的 else
    • 頂層變數宣告
    • 對于未匯出的頂層常量和變數,使用_作為前綴
    • 結構體中的嵌入
    • 使用欄位名初始化結構體
    • 本地變數宣告
    • nil 是一個有效的 slice
    • 小變數作用域
    • 避免引數語意不明確(Avoid Naked Parameters)
    • 使用原始字串字面值,避免轉義
    • 初始化 Struct 參考
    • 初始化 Maps
    • 字串 string format
    • 命名 Printf 樣式的函式
  • 編程模式
    • 表驅動測驗
    • 功能選項

介紹

樣式 (style) 是支配我們代碼的慣例,術語樣式有點用詞不當,因為這些約定涵蓋的范圍不限于由 gofmt 替我們處理的源檔案格式,

本指南的目的是通過詳細描述在 Uber 撰寫 Go 代碼的注意事項來管理這種復雜性,這些規則的存在是為了使代碼庫易于管理,同時仍然允許工程師更有效地使用 Go 語言功能,

該指南最初由 Prashant Varanasi 和 Simon Newton 撰寫,目的是使一些同事能快速使用 Go,多年來,該指南已根據其他人的反饋進行了修改,

本檔案記錄了我們在 Uber 遵循的 Go 代碼中的慣用約定,其中許多是 Go 的通用準則,而其他擴展準則依賴于下面外部的指南:

  1. Effective Go
  2. The Go common mistakes guide

所有代碼都應該通過golintgo vet的檢查并無錯誤,我們建議您將編輯器設定為:

  • 保存時運行 goimports
  • 運行 golintgo vet 檢查錯誤

您可以在以下 Go 編輯器工具支持頁面中找到更為詳細的資訊:
https://github.com/golang/go/wiki/IDEsAndTextEditorPlugins

指導原則

指向 interface 的指標

您幾乎不需要指向介面型別的指標,您應該將介面作為值進行傳遞,在這樣的傳遞程序中,實質上傳遞的底層資料仍然可以是指標,

介面實質上在底層用兩個欄位表示:

  1. 一個指向某些特定型別資訊的指標,您可以將其視為"type",
  2. 資料指標,如果存盤的資料是指標,則直接存盤,如果存盤的資料是一個值,則存盤指向該值的指標,

如果希望介面方法修改基礎資料,則必須使用指標傳遞,

接收器 (receiver) 與介面

使用值接收器的方法既可以通過值呼叫,也可以通過指標呼叫,

例如,

type S struct {
  data string
}

func (s S) Read() string {
  return s.data
}

func (s *S) Write(str string) {
  s.data = https://www.cnblogs.com/sblig/p/str
}

sVals := map[int]S{1: {"A"}}

// 你只能通過值呼叫 Read
sVals[1].Read()

// 這不能編譯通過:
//  sVals[1].Write("test")

sPtrs := map[int]*S{1: {"A"}}

// 通過指標既可以呼叫 Read,也可以呼叫 Write 方法
sPtrs[1].Read()
sPtrs[1].Write("test")

同樣,即使該方法具有值接收器,也可以通過指標來滿足介面,

type F interface {
  f()
}

type S1 struct{}

func (s S1) f() {}

type S2 struct{}

func (s *S2) f() {}

s1Val := S1{}
s1Ptr := &S1{}
s2Val := S2{}
s2Ptr := &S2{}

var i F
i = s1Val
i = s1Ptr
i = s2Ptr

//  下面代碼無法通過編譯,因為 s2Val 是一個值,而 S2 的 f 方法中沒有使用值接收器
//   i = s2Val

Effective Go 中有一段關于 pointers vs. values 的精彩講解,

零值 Mutex 是有效的

零值 sync.Mutexsync.RWMutex 是有效的,所以指向 mutex 的指標基本是不必要的,

BadGood
mu := new(sync.Mutex)
mu.Lock()
var mu sync.Mutex
mu.Lock()

如果你使用結構體指標,mutex 可以非指標形式作為結構體的組成欄位,或者更好的方式是直接嵌入到結構體中,
如果是私有結構體型別或是要實作 Mutex 介面的型別,我們可以使用嵌入 mutex 的方法:

type smap struct {
  sync.Mutex // only for unexported types(僅適用于非匯出型別)

  data map[string]string
}

func newSMap() *smap {
  return &smap{
    data: make(map[string]string),
  }
}

func (m *smap) Get(k string) string {
  m.Lock()
  defer m.Unlock()

  return m.data[k]
}
type SMap struct {
  mu sync.Mutex // 對于匯出型別,請使用私有鎖

  data map[string]string
}

func NewSMap() *SMap {
  return &SMap{
    data: make(map[string]string),
  }
}

func (m *SMap) Get(k string) string {
  m.mu.Lock()
  defer m.mu.Unlock()

  return m.data[k]
}
為私有型別或需要實作互斥介面的型別嵌入, 對于匯出的型別,請使用專用欄位,

在邊界處拷貝 Slices 和 Maps

slices 和 maps 包含了指向底層資料的指標,因此在需要復制它們時要特別注意,

接收 Slices 和 Maps

請記住,當 map 或 slice 作為函式引數傳入時,如果記憶體儲了對它們的參考,則用戶可以對其進行修改,

Bad Good
func (d *Driver) SetTrips(trips []Trip) {
  d.trips = trips
}

trips := ...
d1.SetTrips(trips)

// 你是要修改 d1.trips 嗎?
trips[0] = ...
func (d *Driver) SetTrips(trips []Trip) {
  d.trips = make([]Trip, len(trips))
  copy(d.trips, trips)
}

trips := ...
d1.SetTrips(trips)

// 這里我們修改 trips[0],但不會影響到 d1.trips
trips[0] = ...

回傳 slices 或 maps

同樣,請注意用戶對暴露內部狀態的 map 或 slice 的修改,

BadGood
type Stats struct {
  mu sync.Mutex

  counters map[string]int
}

// Snapshot 回傳當前狀態,
func (s *Stats) Snapshot() map[string]int {
  s.mu.Lock()
  defer s.mu.Unlock()

  return s.counters
}

// snapshot 不再受互斥鎖保護
// 因此對 snapshot 的任何訪問都將受到資料競爭的影響
// 影響 stats.counters
snapshot := stats.Snapshot()
type Stats struct {
  mu sync.Mutex

  counters map[string]int
}

func (s *Stats) Snapshot() map[string]int {
  s.mu.Lock()
  defer s.mu.Unlock()

  result := make(map[string]int, len(s.counters))
  for k, v := range s.counters {
    result[k] = v
  }
  return result
}

// snapshot 現在是一個拷貝
snapshot := stats.Snapshot()

使用 defer 釋放資源

使用 defer 釋放資源,諸如檔案和鎖,

BadGood
p.Lock()
if p.count < 10 {
  p.Unlock()
  return p.count
}

p.count++
newCount := p.count
p.Unlock()

return newCount

// 當有多個 return 分支時,很容易遺忘 unlock
p.Lock()
defer p.Unlock()

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

p.count++
return p.count

// 更可讀

Defer 的開銷非常小,只有在您可以證明函式執行時間處于納秒級的程度時,才應避免這樣做,使用 defer 提升可讀性是值得的,因為使用它們的成本微不足道,尤其適用于那些不僅僅是簡單記憶體訪問的較大的方法,在這些方法中其他計算的資源消耗遠超過 defer

Channel 的 size 要么是 1,要么是無緩沖的

channel 通常 size 應為 1 或是無緩沖的,默認情況下,channel 是無緩沖的,其 size 為零,任何其他尺寸都必須經過嚴格的審查,考慮如何確定大小,是什么阻止了 channel 在負載下被填滿并阻止寫入,以及發生這種情況時發生了什么,

BadGood
// 應該足以滿足任何情況!
c := make(chan int, 64)
// 大小:1
c := make(chan int, 1) // 或者
// 無緩沖 channel,大小為 0
c := make(chan int)

列舉從 1 開始

在 Go 中引入列舉的標準方法是宣告一個自定義型別和一個使用了 iota 的 const 組,由于變數的默認值為 0,因此通常應以非零值開頭列舉,

BadGood
type Operation int

const (
  Add Operation = iota
  Subtract
  Multiply
)

// Add=0, Subtract=1, Multiply=2
type Operation int

const (
  Add Operation = iota + 1
  Subtract
  Multiply
)

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

在某些情況下,使用零值是有意義的(列舉從零開始),例如,當零值是理想的默認行為時,

type LogOutput int

const (
  LogToStdout LogOutput = iota
  LogToFile
  LogToRemote
)

// LogToStdout=0, LogToFile=1, LogToRemote=2

錯誤型別

Go 中有多種宣告錯誤(Error) 的選項:

  • errors.New 對于簡單靜態字串的錯誤
  • fmt.Errorf 用于格式化的錯誤字串
  • 實作 Error() 方法的自定義型別
  • "pkg/errors".Wrap 的 Wrapped errors

回傳錯誤時,請考慮以下因素以確定最佳選擇:

  • 這是一個不需要額外資訊的簡單錯誤嗎?如果是這樣,errors.New 足夠了,

  • 客戶需要檢測并處理此錯誤嗎?如果是這樣,則應使用自定義型別并實作該 Error() 方法,

  • 您是否正在傳播下游函式回傳的錯誤?如果是這樣,請查看本文后面有關錯誤包裝 section on error wrapping 部分的內容,

  • 否則 fmt.Errorf 就可以了,

如果客戶端需要檢測錯誤,并且您已使用創建了一個簡單的錯誤 errors.New,請使用一個錯誤變數,

BadGood
// package foo

func Open() error {
  return errors.New("could not open")
}

// package bar

func use() {
  if err := foo.Open(); err != nil {
    if err.Error() == "could not open" {
      // handle
    } else {
      panic("unknown error")
    }
  }
}
// package foo

var ErrCouldNotOpen = errors.New("could not open")

func Open() error {
  return ErrCouldNotOpen
}

// package bar

if err := foo.Open(); err != nil {
  if err == foo.ErrCouldNotOpen {
    // handle
  } else {
    panic("unknown error")
  }
}

如果您有可能需要客戶端檢測的錯誤,并且想向其中添加更多資訊(例如,它不是靜態字串),則應使用自定義型別,

BadGood
func open(file string) error {
  return fmt.Errorf("file %q not found", file)
}

func use() {
  if err := open(); err != nil {
    if strings.Contains(err.Error(), "not found") {
      // handle
    } else {
      panic("unknown error")
    }
  }
}
type errNotFound struct {
  file string
}

func (e errNotFound) Error() string {
  return fmt.Sprintf("file %q not found", e.file)
}

func open(file string) error {
  return errNotFound{file: file}
}

func use() {
  if err := open(); err != nil {
    if _, ok := err.(errNotFound); ok {
      // handle
    } else {
      panic("unknown error")
    }
  }
}

直接匯出自定義錯誤型別時要小心,因為它們已成為程式包公共 API 的一部分,最好公開匹配器功能以檢查錯誤,

// package foo

type errNotFound struct {
  file string
}

func (e errNotFound) Error() string {
  return fmt.Sprintf("file %q not found", e.file)
}

func IsNotFoundError(err error) bool {
  _, ok := err.(errNotFound)
  return ok
}

func Open(file string) error {
  return errNotFound{file: file}
}

// package bar

if err := foo.Open("foo"); err != nil {
  if foo.IsNotFoundError(err) {
    // handle
  } else {
    panic("unknown error")
  }
}

錯誤包裝 (Error Wrapping)

一個(函式/方法)呼叫失敗時,有三種主要的錯誤傳播方式:

  • 如果沒有要添加的其他背景關系,并且您想要維護原始錯誤型別,則回傳原始錯誤,

  • 添加背景關系,使用 "pkg/errors".Wrap 以便錯誤訊息提供更多背景關系 ,"pkg/errors".Cause 可用于提取原始錯誤,
    Use fmt.Errorf if the callers do not need to detect or handle that specific error case.

  • 如果呼叫者不需要檢測或處理的特定錯誤情況,使用 fmt.Errorf

建議在可能的地方添加背景關系,以使您獲得諸如“呼叫服務 foo:連接被拒絕”之類的更有用的錯誤,而不是諸如“連接被拒絕”之類的模糊錯誤,

在將背景關系添加到回傳的錯誤時,請避免使用“failed to”之類的短語來保持背景關系簡潔,這些短語會陳述明顯的內容,并隨著錯誤在堆疊中的滲透而逐漸堆積:

BadGood
s, err := store.New()
if err != nil {
    return fmt.Errorf(
        "failed to create new store: %s", err)
}
s, err := store.New()
if err != nil {
    return fmt.Errorf(
        "new store: %s", err)
}
failed to x: failed to y: failed to create new store: the error
x: y: new store: the error

但是,一旦將錯誤發送到另一個系統,就應該明確訊息是錯誤訊息(例如使用err標記,或在日志中以”Failed”為前綴),

另請參見 Don't just check errors, handle them gracefully. 不要只是檢查錯誤,要優雅地處理錯誤

處理型別斷言失敗

type assertion 的單個回傳值形式針對不正確的型別將產生 panic,因此,請始終使用“comma ok”的慣用法,

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

不要 panic

在生產環境中運行的代碼必須避免出現 panic,panic 是 cascading failures 級聯失敗的主要根源 ,如果發生錯誤,該函式必須回傳錯誤,并允許呼叫方決定如何處理它,

BadGood
func foo(bar string) {
  if len(bar) == 0 {
    panic("bar must not be empty")
  }
  // ...
}

func main() {
  if len(os.Args) != 2 {
    fmt.Println("USAGE: foo <bar>")
    os.Exit(1)
  }
  foo(os.Args[1])
}
func foo(bar string) error {
  if len(bar) == 0 {
    return errors.New("bar must not be empty")
  }
  // ...
  return nil
}

func main() {
  if len(os.Args) != 2 {
    fmt.Println("USAGE: foo <bar>")
    os.Exit(1)
  }
  if err := foo(os.Args[1]); err != nil {
    panic(err)
  }
}

panic/recover 不是錯誤處理策略,僅當發生不可恢復的事情(例如:nil 參考)時,程式才必須 panic,程式初始化是一個例外:程式啟動時應使程式中止的不良情況可能會引起 panic,

var _statusTemplate = template.Must(template.New("name").Parse("_statusHTML"))

即使在測驗代碼中,也優先使用t.Fatal或者t.FailNow而不是 panic 來確保失敗被標記,

BadGood
// func TestFoo(t *testing.T)

f, err := ioutil.TempFile("", "test")
if err != nil {
  panic("failed to set up test")
}
// func TestFoo(t *testing.T)

f, err := ioutil.TempFile("", "test")
if err != nil {
  t.Fatal("failed to set up test")
}

使用 go.uber.org/atomic

使用 sync/atomic 包的原子操作對原始型別 (int32, int64等)進行操作,因為很容易忘記使用原子操作來讀取或修改變數,

go.uber.org/atomic 通過隱藏基礎型別為這些操作增加了型別安全性,此外,它包括一個方便的atomic.Bool型別,

BadGood
type foo struct {
  running int32  // atomic
}

func (f* foo) start() {
  if atomic.SwapInt32(&f.running, 1) == 1 {
     // already running…
     return
  }
  // start the Foo
}

func (f *foo) isRunning() bool {
  return f.running == 1  // race!
}
type foo struct {
  running atomic.Bool
}

func (f *foo) start() {
  if f.running.Swap(true) {
     // already running…
     return
  }
  // start the Foo
}

func (f *foo) isRunning() bool {
  return f.running.Load()
}

性能

性能方面的特定準則只適用于高頻場景,

優先使用 strconv 而不是 fmt

將原語轉換為字串或從字串轉換時,strconv速度比fmt快,

BadGood
for i := 0; i < b.N; i++ {
  s := fmt.Sprint(rand.Int())
}
for i := 0; i < b.N; i++ {
  s := strconv.Itoa(rand.Int())
}
BenchmarkFmtSprint-4    143 ns/op    2 allocs/op
BenchmarkStrconv-4    64.2 ns/op    1 allocs/op

避免字串到位元組的轉換

不要反復從固定字串創建位元組 slice,相反,請執行一次轉換并捕獲結果,

BadGood
for i := 0; i < b.N; i++ {
  w.Write([]byte("Hello world"))
}
data := []byte("Hello world")
for i := 0; i < b.N; i++ {
  w.Write(data)
}
BenchmarkBad-4   50000000   22.2 ns/op
BenchmarkGood-4  500000000   3.25 ns/op

盡量初始化時指定 Map 容量

在盡可能的情況下,在使用 make() 初始化的時候提供容量資訊

make(map[T1]T2, hint)

make() 提供容量資訊(hint)嘗試在初始化時調整 map 大小,
這減少了在將元素添加到 map 時增長和分配的開銷,
注意,map 不能保證分配 hint 個容量,因此,即使提供了容量,添加元素仍然可以進行分配,

BadGood
m := make(map[string]os.FileInfo)

files, _ := ioutil.ReadDir("./files")
for _, f := range files {
    m[f.Name()] = f
}

files, _ := ioutil.ReadDir("./files")

m := make(map[string]os.FileInfo, len(files))
for _, f := range files {
    m[f.Name()] = f
}

m 是在沒有大小提示的情況下創建的; 在運行時可能會有更多分配,

m 是有大小提示創建的;在運行時可能會有更少的分配,

規范

一致性

本文中概述的一些標準都是客觀性的評估,是根據場景、背景關系、或者主觀性的判斷;

但是最重要的是,保持一致.

一致性的代碼更容易維護、是更合理的、需要更少的學習成本、并且隨著新的約定出現或者出現錯誤后更容易遷移、更新、修復 bug

相反,一個單一的代碼庫會導致維護成本開銷、不確定性和認知偏差,所有這些都會直接導致速度降低、
代碼審查痛苦、而且增加 bug 數量

將這些標準應用于代碼庫時,建議在 package(或更大)級別進行更改,子包級別的應用程式通過將多個樣式引入到同一代碼中,違反了上述關注點,

相似的宣告放在一組

Go 語言支持將相似的宣告放在一個組內,

BadGood
import "a"
import "b"
import (
  "a"
  "b"
)

這同樣適用于常量、變數和型別宣告:

BadGood

const a = 1
const b = 2

var a = 1
var b = 2

type Area float64
type Volume float64
const (
  a = 1
  b = 2
)

var (
  a = 1
  b = 2
)

type (
  Area float64
  Volume float64
)

僅將相關的宣告放在一組,不要將不相關的宣告放在一組,

BadGood
type Operation int

const (
  Add Operation = iota + 1
  Subtract
  Multiply
  ENV_VAR = "MY_ENV"
)
type Operation int

const (
  Add Operation = iota + 1
  Subtract
  Multiply
)

const ENV_VAR = "MY_ENV"

分組使用的位置沒有限制,例如:你可以在函式內部使用它們:

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

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

  ...
}

import 分組

匯入應該分為兩組:

  • 標準庫
  • 其他庫

默認情況下,這是 goimports 應用的分組,

BadGood
import (
  "fmt"
  "os"
  "go.uber.org/atomic"
  "golang.org/x/sync/errgroup"
)
import (
  "fmt"
  "os"

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

包名

當命名包時,請按下面規則選擇一個名稱:

  • 全部小寫,沒有大寫或下劃線,
  • 大多數使用命名匯入的情況下,不需要重命名,
  • 簡短而簡潔,請記住,在每個使用的地方都完整標識了該名稱,
  • 不用復數,例如net/url,而不是net/urls
  • 不要用“common”,“util”,“shared”或“lib”,這些是不好的,資訊量不足的名稱,

另請參閱 Package Names 和 Go 包樣式指南.

函式名

我們遵循 Go 社區關于使用 MixedCaps 作為函式名 的約定,有一個例外,為了對相關的測驗用例進行分組,函式名可能包含下劃線,如:TestMyFunction_WhatIsBeingTested.

匯入別名

如果程式包名稱與匯入路徑的最后一個元素不匹配,則必須使用匯入別名,

import (
  "net/http"

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

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

BadGood
import (
  "fmt"
  "os"

  nettrace "golang.net/x/trace"
)
import (
  "fmt"
  "os"
  "runtime/trace"

  nettrace "golang.net/x/trace"
)

函式分組與順序

  • 函式應按粗略的呼叫順序排序,
  • 同一檔案中的函式應按接收者分組,

因此,匯出的函式應先出現在檔案中,放在struct, const, var定義的后面,

在定義型別之后,但在接收者的其余方法之前,可能會出現一個 newXYZ()/NewXYZ()

由于函式是按接收者分組的,因此普通工具函式應在檔案末尾出現,

BadGood
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 {...}

減少嵌套

代碼應通過盡可能先處理錯誤情況/特殊情況并盡早回傳或繼續回圈來減少嵌套,減少嵌套多個級別的代碼的代碼量,

BadGood
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,

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

頂層變數宣告

在頂層,使用標準var關鍵字,請勿指定型別,除非它與運算式的型別不同,

BadGood
var _s string = F()

func F() string { return "A" }
var _s = F()
// 由于 F 已經明確了回傳一個字串型別,因此我們沒有必要顯式指定_s 的型別
// 還是那種型別

func F() string { return "A" }

如果運算式的型別與所需的型別不完全匹配,請指定型別,

type myError struct{}

func (myError) Error() string { return "error" }

func F() myError { return myError{} }

var _e error = F()
// F 回傳一個 myError 型別的實體,但是我們要 error 型別

對于未匯出的頂層常量和變數,使用_作為前綴

在未匯出的頂級varsconsts, 前面加上前綴_,以使它們在使用時明確表示它們是全域符號,

例外:未匯出的錯誤值,應以err開頭,

基本依據:頂級變數和常量具有包范圍作用域,使用通用名稱可能很容易在其他檔案中意外使用錯誤的值,

BadGood
// foo.go

const (
  defaultPort = 8080
  defaultUser = "user"
)

// bar.go

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

  // We will not see a compile error if the first line of
  // Bar() is deleted.
}
// foo.go

const (
  _defaultPort = 8080
  _defaultUser = "user"
)

結構體中的嵌入

嵌入式型別(例如 mutex)應位于結構體內的欄位串列的頂部,并且必須有一個空行將嵌入式欄位與常規欄位分隔開,

BadGood
type Client struct {
  version int
  http.Client
}
type Client struct {
  http.Client

  version int
}

使用欄位名初始化結構體

初始化結構體時,幾乎始終應該指定欄位名稱,現在由 go vet 強制執行,

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

例外:如果有 3 個或更少的欄位,則可以在測驗表中省略欄位名稱,

tests := []struct{
  op Operation
  want string
}{
  {Add, "add"},
  {Subtract, "subtract"},
}

本地變數宣告

如果將變數明確設定為某個值,則應使用短變數宣告形式 (:=),

BadGood
var s = "foo"
s := "foo"

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

BadGood
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 是一個有效的 slice

nil 是一個有效的長度為 0 的 slice,這意味著,

  • 您不應明確回傳長度為零的切片,應該回傳nil 來代替,

    BadGood
    if x == "" {
      return []int{}
    }
    
    if x == "" {
      return nil
    }
    
  • 要檢查切片是否為空,請始終使用len(s) == 0,而非 nil

    BadGood
    func isEmpty(s []string) bool {
      return s == nil
    }
    
    func isEmpty(s []string) bool {
      return len(s) == 0
    }
    
  • 零值切片(用var宣告的切片)可立即使用,無需呼叫make()創建,

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

小變數作用域

如果有可能,盡量縮小變數作用范圍,除非它與 減少嵌套的規則沖突,

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

如果需要在 if 之外使用函式呼叫的結果,則不應嘗試縮小范圍,

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

  fmt.Println(cfg)
  return nil
} else {
  return err
}
data, err := ioutil.ReadFile(name)
if err != nil {
   return err
}

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

fmt.Println(cfg)
return nil

避免引數語意不明確(Avoid Naked Parameters)

函式呼叫中的意義不明確的引數可能會損害可讀性,當引數名稱的含義不明顯時,請為引數添加 C 樣式注釋 (/* ... */)

BadGood
// func printInfo(name string, isLocal, done bool)

printInfo("foo", true, true)
// func printInfo(name string, isLocal, done bool)

printInfo("foo", true /* isLocal */, true /* done */)

對于上面的示例代碼,還有一種更好的處理方式是將上面的 bool 型別換成自定義型別,將來,該引數可以支持不僅僅局限于兩個狀態(true/false),

type Region int

const (
  UnknownRegion Region = iota
  Local
)

type Status int

const (
  StatusReady = iota + 1
  StatusDone
  // Maybe we will have a StatusInProgress in the future.
)

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

使用原始字串字面值,避免轉義

Go 支持使用 原始字串字面值,也就是 " ` " 來表示原生字串,在需要轉義的場景下,我們應該盡量使用這種方案來替換,

可以跨越多行并包含引號,使用這些字串可以避免更難閱讀的手工轉義的字串,

BadGood
wantError := "unknown name:\"test\""
wantError := `unknown error:"test"`

初始化 Struct 參考

在初始化結構參考時,請使用&T{}代替new(T),以使其與結構體初始化一致,

BadGood
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 后添加大小提示,

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

宣告和初始化看起來非常相似的,

宣告和初始化看起來差別非常大,

在盡可能的情況下,請在初始化時提供 map 容量大小,詳細請看 盡量初始化時指定 Map 容量,

另外,如果 map 包含固定的元素串列,則使用 map literals(map 初始化串列) 初始化映射,

BadGood
m := make(map[T1]T2, 3)
m[k1] = v1
m[k2] = v2
m[k3] = v3
m := map[T1]T2{
  k1: v1,
  k2: v2,
  k3: v3,
}

基本準則是:在初始化時使用 map 初始化串列 來添加一組固定的元素,否則使用 make (如果可以,請盡量指定 map 容量),

字串 string format

如果你為Printf-style 函式宣告格式字串,請將格式化字串放在外面,并將其設定為const常量,

這有助于go vet對格式字串執行靜態分析,

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

命名 Printf 樣式的函式

宣告Printf-style 函式時,請確保go vet可以檢測到它并檢查格式字串,

這意味著您應盡可能使用預定義的Printf-style 函式名稱,go vet將默認檢查這些,有關更多資訊,請參見 Printf 系列,

如果不能使用預定義的名稱,請以 f 結束選擇的名稱:Wrapf,而不是Wrapgo vet可以要求檢查特定的 Printf 樣式名稱,但名稱必須以f結尾,

$ go vet -printfuncs=wrapf,statusf

另請參閱 go vet: Printf family check.

編程模式

表驅動測驗

當測驗邏輯是重復的時候,通過 subtests 使用 table 驅動的方式撰寫 case 代碼看上去會更簡潔,

BadGood
// func TestSplitHostPort(t *testing.T)

host, port, err := net.SplitHostPort("192.0.2.0:8000")
require.NoError(t, err)
assert.Equal(t, "192.0.2.0", host)
assert.Equal(t, "8000", port)

host, port, err = net.SplitHostPort("192.0.2.0:http")
require.NoError(t, err)
assert.Equal(t, "192.0.2.0", host)
assert.Equal(t, "http", port)

host, port, err = net.SplitHostPort(":8000")
require.NoError(t, err)
assert.Equal(t, "", host)
assert.Equal(t, "8000", port)

host, port, err = net.SplitHostPort("1:8")
require.NoError(t, err)
assert.Equal(t, "1", host)
assert.Equal(t, "8", port)
// func TestSplitHostPort(t *testing.T)

tests := []struct{
  give     string
  wantHost string
  wantPort string
}{
  {
    give:     "192.0.2.0:8000",
    wantHost: "192.0.2.0",
    wantPort: "8000",
  },
  {
    give:     "192.0.2.0:http",
    wantHost: "192.0.2.0",
    wantPort: "http",
  },
  {
    give:     ":8000",
    wantHost: "",
    wantPort: "8000",
  },
  {
    give:     "1:8",
    wantHost: "1",
    wantPort: "8",
  },
}

for _, tt := range tests {
  t.Run(tt.give, func(t *testing.T) {
    host, port, err := net.SplitHostPort(tt.give)
    require.NoError(t, err)
    assert.Equal(t, tt.wantHost, host)
    assert.Equal(t, tt.wantPort, port)
  })
}

很明顯,使用 test table 的方式在代碼邏輯擴展的時候,比如新增 test case,都會顯得更加的清晰,

我們遵循這樣的約定:將結構體切片稱為tests, 每個測驗用例稱為tt,此外,我們鼓勵使用givewant前綴說明每個測驗用例的輸入和輸出值,

tests := []struct{
  give     string
  wantHost string
  wantPort string
}{
  // ...
}

for _, tt := range tests {
  // ...
}

功能選項

功能選項是一種模式,您可以在其中宣告一個不透明 Option 型別,該型別在某些內部結構中記錄資訊,您接受這些選項的可變編號,并根據內部結構上的選項記錄的全部資訊采取行動,

將此模式用于您需要擴展的建構式和其他公共 API 中的可選引數,尤其是在這些功能上已經具有三個或更多引數的情況下,

BadGood
// package db

func Connect(
  addr string,
  timeout time.Duration,
  caching bool,
) (*Connection, error) {
  // ...
}

// Timeout and caching must always be provided,
// even if the user wants to use the default.

db.Connect(addr, db.DefaultTimeout, db.DefaultCaching)
db.Connect(addr, newTimeout, db.DefaultCaching)
db.Connect(addr, db.DefaultTimeout, false /* caching */)
db.Connect(addr, newTimeout, false /* caching */)
type options struct {
  timeout time.Duration
  caching bool
}

// Option overrides behavior of Connect.
type Option interface {
  apply(*options)
}

type optionFunc func(*options)

func (f optionFunc) apply(o *options) {
  f(o)
}

func WithTimeout(t time.Duration) Option {
  return optionFunc(func(o *options) {
    o.timeout = t
  })
}

func WithCaching(cache bool) Option {
  return optionFunc(func(o *options) {
    o.caching = cache
  })
}

// Connect creates a connection.
func Connect(
  addr string,
  opts ...Option,
) (*Connection, error) {
  options := options{
    timeout: defaultTimeout,
    caching: defaultCaching,
  }

  for _, o := range opts {
    o.apply(&options)
  }

  // ...
}

// Options must be provided only if needed.

db.Connect(addr)
db.Connect(addr, db.WithTimeout(newTimeout))
db.Connect(addr, db.WithCaching(false))
db.Connect(
  addr,
  db.WithCaching(false),
  db.WithTimeout(newTimeout),
)

還可以參考下面資料:

  • Self-referential functions and the design of options

  • Functional options for friendly APIs

本文由zshipu.com學習筆記或整理或轉載,如有侵權請聯系,必改之,

轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/55292.html

標籤:其他

上一篇:一些用著方便的谷歌插件

下一篇:如何安全地洗掉固態硬碟(SSD)、U盤等閃存設備上的檔案使其無法恢復?

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 網閘典型架構簡述

    網閘架構一般分為兩種:三主機的三系統架構網閘和雙主機的2+1架構網閘。 三主機架構分別為內端機、外端機和仲裁機。三機無論從軟體和硬體上均各自獨立。首先從硬體上來看,三機都用各自獨立的主板、記憶體及存盤設備。從軟體上來看,三機有各自獨立的作業系統。這樣能達到完全的三機獨立。對于“2+1”系統,“2”分為 ......

    uj5u.com 2020-09-10 02:00:44 more
  • 如何從xshell上傳檔案到centos linux虛擬機里

    如何從xshell上傳檔案到centos linux虛擬機里及:虛擬機CentOs下執行 yum -y install lrzsz命令,出現錯誤:鏡像無法找到軟體包 前言 一、安裝lrzsz步驟 二、上傳檔案 三、遇到的問題及解決方案 總結 前言 提示:其實很簡單,往虛擬機上安裝一個上傳檔案的工具 ......

    uj5u.com 2020-09-10 02:00:47 more
  • 一、SQLMAP入門

    一、SQLMAP入門 1、判斷是否存在注入 sqlmap.py -u 網址/id=1 id=1不可缺少。當注入點后面的引數大于兩個時。需要加雙引號, sqlmap.py -u "網址/id=1&uid=1" 2、判斷文本中的請求是否存在注入 從文本中加載http請求,SQLMAP可以從一個文本檔案中 ......

    uj5u.com 2020-09-10 02:00:50 more
  • Metasploit 簡單使用教程

    metasploit 簡單使用教程 浩先生, 2020-08-28 16:18:25 分類專欄: kail 網路安全 linux 文章標簽: linux資訊安全 編輯 著作權 metasploit 使用教程 前言 一、Metasploit是什么? 二、準備作業 三、具體步驟 前言 Msfconsole ......

    uj5u.com 2020-09-10 02:00:53 more
  • 游戲逆向之驅動層與用戶層通訊

    驅動層代碼: #pragma once #include <ntifs.h> #define add_code CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,METHOD_BUFFERED,FILE_ANY_ACCESS) /* 更多游戲逆向視頻www.yxfzedu.com ......

    uj5u.com 2020-09-10 02:00:56 more
  • 北斗電力時鐘(北斗授時服務器)讓網路資料更精準

    北斗電力時鐘(北斗授時服務器)讓網路資料更精準 北斗電力時鐘(北斗授時服務器)讓網路資料更精準 京準電子科技官微——ahjzsz 近幾年,資訊技術的得了快速發展,互聯網在逐漸普及,其在人們生活和生產中都得到了廣泛應用,并且取得了不錯的應用效果。計算機網路資訊在電力系統中的應用,一方面使電力系統的運行 ......

    uj5u.com 2020-09-10 02:01:03 more
  • 【CTF】CTFHub 技能樹 彩蛋 writeup

    ?碎碎念 CTFHub:https://www.ctfhub.com/ 筆者入門CTF時時剛開始刷的是bugku的舊平臺,后來才有了CTFHub。 感覺不論是網頁UI設計,還是題目質量,賽事跟蹤,工具軟體都做得很不錯。 而且因為獨到的金幣制度的確讓人有一種想去刷題賺金幣的感覺。 個人還是非常喜歡這個 ......

    uj5u.com 2020-09-10 02:04:05 more
  • 02windows基礎操作

    我學到了一下幾點 Windows系統目錄結構與滲透的作用 常見Windows的服務詳解 Windows埠詳解 常用的Windows注冊表詳解 hacker DOS命令詳解(net user / type /md /rd/ dir /cd /net use copy、批處理 等) 利用dos命令制作 ......

    uj5u.com 2020-09-10 02:04:18 more
  • 03.Linux基礎操作

    我學到了以下幾點 01Linux系統介紹02系統安裝,密碼啊破解03Linux常用命令04LAMP 01LINUX windows: win03 8 12 16 19 配置不繁瑣 Linux:redhat,centos(紅帽社區版),Ubuntu server,suse unix:金融機構,證券,銀 ......

    uj5u.com 2020-09-10 02:04:30 more
  • 05HTML

    01HTML介紹 02頭部標簽講解03基礎標簽講解04表單標簽講解 HTML前段語言 js1.了解代碼2.根據代碼 懂得挖掘漏洞 (POST注入/XSS漏洞上傳)3.黑帽seo 白帽seo 客戶網站被黑帽植入劫持代碼如何處理4.熟悉html表單 <html><head><title>TDK標題,描述 ......

    uj5u.com 2020-09-10 02:04:36 more
最新发布
  • 2023年最新微信小程式抓包教程

    01 開門見山 隔一個月發一篇文章,不過分。 首先回顧一下《微信系結手機號資料庫被脫庫事件》,我也是第一時間得知了這個訊息,然后跟蹤了整件事情的經過。下面是這起事件的相關截圖以及近日流出的一萬條資料樣本: 個人認為這件事也沒什么,還不如關注一下之前45億快遞資料查詢渠道疑似在近日復活的訊息。 訊息是 ......

    uj5u.com 2023-04-20 08:48:24 more
  • web3 產品介紹:metamask 錢包 使用最多的瀏覽器插件錢包

    Metamask錢包是一種基于區塊鏈技術的數字貨幣錢包,它允許用戶在安全、便捷的環境下管理自己的加密資產。Metamask錢包是以太坊生態系統中最流行的錢包之一,它具有易于使用、安全性高和功能強大等優點。 本文將詳細介紹Metamask錢包的功能和使用方法。 一、 Metamask錢包的功能 數字資 ......

    uj5u.com 2023-04-20 08:47:46 more
  • vulnhub_Earth

    前言 靶機地址->>>vulnhub_Earth 攻擊機ip:192.168.20.121 靶機ip:192.168.20.122 參考文章 https://www.cnblogs.com/Jing-X/archive/2022/04/03/16097695.html https://www.cnb ......

    uj5u.com 2023-04-20 07:46:20 more
  • 從4k到42k,軟體測驗工程師的漲薪史,給我看哭了

    清明節一過,盲猜大家已經無心上班,在數著日子準備過五一,但一想到銀行卡里的余額……瞬間心情就不美麗了。最近,2023年高校畢業生就業調查顯示,本科畢業月平均起薪為5825元。調查一出,便有很多同學表示自己又被平均了。看著這一資料,不免讓人想到前不久中國青年報的一項調查:近六成大學生認為畢業10年內會 ......

    uj5u.com 2023-04-20 07:44:00 more
  • 最新版本 Stable Diffusion 開源 AI 繪畫工具之中文自動提詞篇

    🎈 標簽生成器 由于輸入正向提示詞 prompt 和反向提示詞 negative prompt 都是使用英文,所以對學習母語的我們非常不友好 使用網址:https://tinygeeker.github.io/p/ai-prompt-generator 這個網址是為了讓大家在使用 AI 繪畫的時候 ......

    uj5u.com 2023-04-20 07:43:36 more
  • 漫談前端自動化測驗演進之路及測驗工具分析

    隨著前端技術的不斷發展和應用程式的日益復雜,前端自動化測驗也在不斷演進。隨著 Web 應用程式變得越來越復雜,自動化測驗的需求也越來越高。如今,自動化測驗已經成為 Web 應用程式開發程序中不可或缺的一部分,它們可以幫助開發人員更快地發現和修復錯誤,提高應用程式的性能和可靠性。 ......

    uj5u.com 2023-04-20 07:43:16 more
  • CANN開發實踐:4個DVPP記憶體問題的典型案例解讀

    摘要:由于DVPP媒體資料處理功能對存放輸入、輸出資料的記憶體有更高的要求(例如,記憶體首地址128位元組對齊),因此需呼叫專用的記憶體申請介面,那么本期就分享幾個關于DVPP記憶體問題的典型案例,并給出原因分析及解決方法。 本文分享自華為云社區《FAQ_DVPP記憶體問題案例》,作者:昇騰CANN。 DVPP ......

    uj5u.com 2023-04-20 07:43:03 more
  • msf學習

    msf學習 以kali自帶的msf為例 一、msf核心模塊與功能 msf模塊都放在/usr/share/metasploit-framework/modules目錄下 1、auxiliary 輔助模塊,輔助滲透(埠掃描、登錄密碼爆破、漏洞驗證等) 2、encoders 編碼器模塊,主要包含各種編碼 ......

    uj5u.com 2023-04-20 07:42:59 more
  • Halcon軟體安裝與界面簡介

    1. 下載Halcon17版本到到本地 2. 雙擊安裝包后 3. 步驟如下 1.2 Halcon軟體安裝 界面分為四大塊 1. Halcon的五個助手 1) 影像采集助手:與相機連接,設定相機引數,采集影像 2) 標定助手:九點標定或是其它的標定,生成標定檔案及內參外參,可以將像素單位轉換為長度單位 ......

    uj5u.com 2023-04-20 07:42:17 more
  • 在MacOS下使用Unity3D開發游戲

    第一次發博客,先發一下我的游戲開發環境吧。 去年2月份買了一臺MacBookPro2021 M1pro(以下簡稱mbp),這一年來一直在用mbp開發游戲。我大致分享一下我的開發工具以及使用體驗。 1、Unity 官網鏈接: https://unity.cn/releases 我一般使用的Apple ......

    uj5u.com 2023-04-20 07:40:19 more