文章目錄
引言
樣式是支配我們代碼的慣例。 術語“樣式”有點用詞不當,因爲這些約定不僅僅涵蓋那些可以由gofmt替我們處理的源文件格式。
本指南的目的是通過詳細描述在Uber編寫Go代碼的注意事項來管理這種複雜性。 這些規則的存在是爲了使代碼庫易於管理,同時仍然允許工程師有效地使用Go語言功能。
該指南最初由Prashant Varanasi和Simon Newton編寫,目的是使一些同事快速使用Go。 多年來,已根據其他人的反饋進行了修改。
本文檔記錄了我們在Uber遵循的Go代碼中的慣用約定。 其中許多是Go的通用準則,而其他準則則依賴於外部資源:
通過golint
和go vet
運行時,所有代碼均應無錯誤。 我們建議您將編輯器設置爲:
- 在保存時運行
goimports
- 運行
golint
和go vet
以檢查錯誤
您可以在以下位置的Go工具的編輯器支持中找到信息:https://github.com/golang/go/wiki/IDEsAndTextEditorPlugins
準則
Pointers to Interfaces
您幾乎不需要指向接口的指針。 您應該將接口作爲值傳遞-底層數據仍然可以是指針。
一個接口是兩個字段:
- 指向某些特定類型信息的指針。 您可以將其視爲“類型”。
- 數據指針。 如果存儲的數據是指針,則直接存儲。 如果存儲的數據是一個值,則存儲指向該值的指針。
如果要接口方法修改基礎數據,則必須使用指針。
接收器和接口
具有值接收器的方法可以在指針和值上調用。
例如
type S struct {
data string
}
func (s S) Read() string {
return s.data
}
func (s *S) Write(str string) {
s.data = str
}
sVals := map[int]S{1: {"A"}}
// You can only call Read using a value
sVals[1].Read()
// This will not compile:
// sVals[1].Write("test")
sPtrs := map[int]*S{1: {"A"}}
// You can call both Read and Write using a pointer
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
// The following doesn't compile, since s2Val is a value, and there is no value receiver for f.
// i = s2Val
Effective Go has a good write up on Pointers vs. Values.
零值互斥是有效的
sync.Mutex
和sync.RWMutex
的零值是有效的,因此不需要指向互斥量的指針。
- 推薦使用:
var mu sync.Mutex
mu.Lock()
- 不推薦使用:
mu := new(sync.Mutex)
mu.Lock()
如果通過指針使用結構,則互斥體可以是非指針字段,或者最好直接嵌入到該結構中。
- 爲專用類型或需要實現Mutex接口的類型嵌入。
type smap struct {
sync.Mutex
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
- 推薦
func (d *Driver) SetTrips(trips []Trip) {
d.trips = make([]Trip, len(trips))
copy(d.trips, trips)
}
trips := ...
d1.SetTrips(trips)
// We can now modify trips[0] without affecting d1.trips.
trips[0] = ...
- 不推薦
func (d *Driver) SetTrips(trips []Trip) {
d.trips = trips
}
trips := ...
d1.SetTrips(trips)
// Did you mean to modify d1.trips?
trips[0] = ...
返回時Slices 和 Maps
同樣,請注意用戶對顯示內部狀態的map或切片的修改。
- 推薦
type Stats struct {
sync.Mutex
counters map[string]int
}
func (s *Stats) Snapshot() map[string]int {
s.Lock()
defer s.Unlock()
result := make(map[string]int, len(s.counters))
for k, v := range s.counters {
result[k] = v
}
return result
}
// Snapshot is now a copy.
snapshot := stats.Snapshot()
- 不推薦
type Stats struct {
sync.Mutex
counters map[string]int
}
// Snapshot returns the current stats.
func (s *Stats) Snapshot() map[string]int {
s.Lock()
defer s.Unlock()
return s.counters
}
// snapshot is no longer protected by the lock!
snapshot := stats.Snapshot()
用defer處理清理工作
使用defer清理資源,例如文件和鎖。
- 推薦
p.Lock()
defer p.Unlock()
if p.count < 10 {
return p.count
}
p.count++
return p.count
// more readable
- 不推薦
p.Lock()
if p.count < 10 {
p.Unlock()
return p.count
}
p.count++
newCount := p.count
p.Unlock()
return newCount
// easy to miss unlocks due to multiple returns
Defer的開銷非常小,只有在您可以證明函數執行時間處於納秒級的程度時,才應避免這樣做。 比起微不足道的成本,使用defer的可讀性是值得的。 對於具有比簡單的內存訪問更多的更大的方法尤其如此,其他方法的計算比defer
要重要得多。
Channel的通道大小爲1 或 無緩衝
通道通常應爲1大小或無緩衝。 默認情況下,通道是無緩衝的,大小爲零。 任何其他大小都必須經過嚴格的審查。 考慮如何確定大小,什麼阻塞通道在負載下填滿並阻塞寫入器,以及發生這種情況時會發生什麼。
- 推薦
// Size of one
c := make(chan int, 1) // or
// Unbuffered channel, size of zero
c := make(chan int)
- 不推薦
// Ought to be enough for anybody!
c := make(chan int, 64)
從1 開始枚舉
在Go中引入枚舉的標準方法是聲明自定義類型和使用iota
組合const
。 由於變量的默認值爲0,因此通常應以非零值開頭枚舉。
- 推薦
type Operation int
const (
Add Operation = iota + 1
Subtract
Multiply
)
// Add=1, Subtract=2, Multiply=3
- 不推薦
type Operation int
const (
Add Operation = iota
Subtract
Multiply
)
// Add=0, Subtract=1, Multiply=2
在某些情況下,使用零值是有意義的,例如,當零值是理想的默認行爲時。
type LogOutput int
const (
LogToStdout LogOutput = iota
LogToFile
LogToRemote
)
// LogToStdout=0, LogToFile=1, LogToRemote=2
錯誤類型
有多種聲明錯誤的選項:
errors.New
對於簡單靜態字符串的錯誤fmt.Errorf
用於格式化的錯誤字符串- 實現
Error()
方法的自定義類型 - 包裝錯誤使用
"pkg/errors".Wrap
返回錯誤時,請考慮以下因素以確定最佳選擇: - 這是一個不需要額外信息的簡單錯誤嗎?如果是,
errors.New
可以滿足 - 客戶需要檢測並處理此錯誤嗎? 如果是這樣,則應使用自定義類型,並實現
Error()
方法。 - 您是否正在傳播下游函數返回的錯誤?如果是,查看section on error wrapping.
- 否則,
fmt.Errorf
就ok。
如果客戶端需要檢測錯誤,並且您已經使用errors創建了一個簡單的錯誤,請使用var。
- 推薦
// 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")
}
}
- 不推薦
// 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")
}
}
}
如果您有可能需要客戶端檢測的錯誤,並且想向其添加更多信息(例如,它不是靜態字符串),則應該使用自定義類型。
- 推薦
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")
}
}
}
- 不推薦
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")
}
}
}
直接導出自定義錯誤類型時要小心,因爲它們已成爲程序包公共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")
}
}
錯誤包裝
調用失敗時,有三種主要的錯誤傳播方式:
- 如果沒有要添加的其他上下文,並且您想要維護原始錯誤類型,則返回原始錯誤。
- 使用
pkg / errors
增加上下文。包裝以便錯誤消息提供更多上下而且pkg / errors.Cause
可用於提取原始錯誤。 - 如果調用方不需要檢測或處理該特定錯誤情況使用
fmt.Errorf
。
使用示例: - 推薦
s, err := store.New()
if err != nil {
return fmt.Errorf(
"new store: %s", err)
}
//x: y: new store: the error
- 不推薦
s, err := store.New()
if err != nil {
return fmt.Errorf(
"failed to create new store: %s", err)
}
//failed to x: failed to y: failed to create new store: the error
但是,一旦將錯誤發送到另一個系統,就應該清楚該消息是一個錯誤(例如,日誌中的err
標記或“Failed”前綴)。
處理類型斷言失敗處理類型斷言失敗
類型斷言的單個返回值形式對不正確的類型將會panic
。 因此,請始終使用“,ok”的習慣用法。
- 推薦使用
t, ok := i.(string)
if !ok {
// handle the error gracefully
}
- 不推薦單個返回值:
t := i.(string)
不要使用panic
在生產中運行的代碼必須避免出現panic
情況。panic
是級聯失敗的主要根源。 如果發生錯誤,該函數必須返回錯誤,並允許調用方決定如何處理它。
- 推薦
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)
}
}
- 不推薦
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])
}
Panic/recover
不是錯誤處理策略。僅當發生不可恢復的事情(例如取消nil引用)時,程序才必須panic
。程序初始化是一個例外:程序啓動時應使程序中止的不良情況可能會引起panic
。
var _statusTemplate = template.Must(template.New("name").Parse("_statusHTML"))
即使在測試中,也要優先選擇t.Fatal
或t.FailNow
來代替panic
,以確保測試標記爲失敗。
- 推薦
// func TestFoo(t *testing.T)
f, err := ioutil.TempFile("", "test")
if err != nil {
t.Fatal("failed to set up test")
}
- 不推薦
// func TestFoo(t *testing.T)
f, err := ioutil.TempFile("", "test")
if err != nil {
panic("failed to set up test")
}
go.uber.org/atomic
使用sync / atomic
包的原子操作對原始類型(int32,int64等)進行操作,因此很容易忘記使用原子操作來讀取或修改變量。
go.uber.org/atomic
通過隱藏基礎類型爲這些操作增加了類型安全性。 另外,它包括一個方便的atomic.Bool
類型。
- 推薦
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()
}
- 不推薦官方:
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!
}
性能
優先使用strconv而不是fmt
- 推薦
# BenchmarkStrconv-4 64.2 ns/op 1 allocs/op
for i := 0; i < b.N; i++ {
s := strconv.Itoa(rand.Int())
}
- 不推薦
# BenchmarkFmtSprint-4 143 ns/op 2 allocs/op
for i := 0; i < b.N; i++ {
s := fmt.Sprint(rand.Int())
}
避免string到[]byte的轉換
不要重複從固定字符串創建字節片。相反,請執行一次轉換並捕獲結果。
- 推薦
# BenchmarkGood-4 500000000 3.25 ns/op
data := []byte("Hello world")
for i := 0; i < b.N; i++ {
w.Write(data)
}
- 不推薦
# BenchmarkBad-4 50000000 22.2 ns/op
for i := 0; i < b.N; i++ {
w.Write([]byte("Hello world"))
}
風格
組相似聲明
- 推薦
import (
"a"
"b"
)
const (
a = 1
b = 2
)
var (
a = 1
b = 2
)
type (
Area float64
Volume float64
)
- 不推薦
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
)
const ENV_VAR = "MY_ENV"
- 不推薦
type Operation int
const (
Add Operation = iota + 1
Subtract
Multiply
ENV_VAR = "MY_ENV"
)
組不受使用位置的限制。
例如,您可以在函數內部使用它們。
func f() string {
var (
red = color.New(0xff0000)
green = color.New(0x00ff00)
blue = color.New(0x0000ff)
)
...
}
import 順序
應該有兩個導入組:
- 標準庫
- 其他一切
默認情況下,這是goimports
應用的分組。
import (
"fmt"
"os"
"go.uber.org/atomic"
"golang.org/x/sync/errgroup"
)
包命名
命名包時,按如下原則選擇:
- 全部小寫。沒有大寫或下劃線
- 大多數情況不需要在導入時重命名
- 簡短而簡潔。請記住,在每個調用站點上都完整標識了該名稱。
- 不復數。例如,net / url,而不是net / url。
- 不用
common
,util
,shared
或lib
。這些是不好的,無用的名稱。
可以參考 Package Names和 style guideline for Go packages.
函數命名
導入別名
函數分組排序
- 函數應按粗略的調用順序排序。
- 文件中的函數應按接收者分組。
因此,導出的函數應首先出現在文件中,然後是struct
,const
,var
定義。
減少嵌套
代碼應通過儘可能先處理錯誤情況/特殊情況並儘早返回或繼續循環來減少嵌套。減少嵌套多個級別的代碼量。
不必要的else
頂級變量聲明
在頂層,使用標準的var關鍵字。請勿指定類型,除非它與表達式的類型不同。
使用_
前綴未導出的全局變量
嵌入結構
type Client struct {
http.Client
version int
}
使用字段名稱初始化結構
## suggest
k := User{
FirstName: "John",
LastName: "Doe",
Admin: true,
}
## no suggest
k := User{"John", "Doe", true}
- 例外: 如果字段數少於或等於3,則測試表中的字段名可以省略。
局部變量聲明
如果將變量顯式設置爲某個值,則應使用短變量聲明:=
。
- 例外:但是,在某些情況下,使用var關鍵字時默認值會更清晰。例如,聲明爲空片。
nil
是有效切片
nil
是長度爲0的有效切片。
縮小變量範圍
# suggest
if err := ioutil.WriteFile(name, data, 0644); err != nil {
return err
}
# no suggest
err := ioutil.WriteFile(name, data, 0644)
if err != nil {
return err
}
初始化結構引用
初始化結構引用時,請使用&T{}
而不是new(T)
,以使其與結構初始化一致。