Uber Go 語言編碼規(guī)范
Uber 是一家美國硅谷的科技公司,也是 Go 語言的早期 adopter。其開源了很多 golang 項目,諸如被 Gopher 圈熟知的 zap、jaeger 等。2018 年年末 Uber 將內部的 Go 風格規(guī)范 開源到 GitHub,經過一年的積累和更新,該規(guī)范已經初具規(guī)模,并受到廣大 Gopher 的關注。本文是該規(guī)范的中文版本。本版本會根據原版實時更新。
版本
- 當前更新版本:2019-11-13 版本地址:commit:#71
- 如果您發(fā)現任何更新、問題或改進,請隨時 fork 和 PR
- Please feel free to fork and PR if you find any updates, issues or improvement.
目錄
介紹
樣式 (style) 是支配我們代碼的慣例。術語樣式有點用詞不當,因為這些約定涵蓋的范圍不限于由 gofmt 替我們處理的源文件格式。
本指南的目的是通過詳細描述在 Uber 編寫 Go 代碼的注意事項來管理這種復雜性。這些規(guī)則的存在是為了使代碼庫易于管理,同時仍然允許工程師更有效地使用 Go 語言功能。
該指南最初由 Prashant Varanasi 和 Simon Newton 編寫,目的是使一些同事能快速使用 Go。多年來,該指南已根據其他人的反饋進行了修改。
本文檔記錄了我們在 Uber 遵循的 Go 代碼中的慣用約定。其中許多是 Go 的通用準則,而其他擴展準則依賴于下面外部的指南:
所有代碼都應該通過golint和go vet的檢查并無錯誤。我們建議您將編輯器設置為:
- 保存時運行
goimports - 運行
golint和go vet檢查錯誤
您可以在以下 Go 編輯器工具支持頁面中找到更為詳細的信息:
https://github.com/golang/go/wiki/IDEsAndTextEditorPlugins
指導原則
指向 interface 的指針
您幾乎不需要指向接口類型的指針。您應該將接口作為值進行傳遞,在這樣的傳遞過程中,實質上傳遞的底層數據仍然可以是指針。
接口實質上在底層用兩個字段表示:
- 一個指向某些特定類型信息的指針。您可以將其視為"type"。
- 數據指針。如果存儲的數據是指針,則直接存儲。如果存儲的數據是一個值,則存儲指向該值的指針。
如果希望接口方法修改基礎數據,則必須使用指針傳遞。
接收器 (receiver) 與接口
使用值接收器的方法既可以通過值調用,也可以通過指針調用。
例如,
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"}}
// 你只能通過值調用 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.Mutex 和 sync.RWMutex 是有效的。所以指向 mutex 的指針基本是不必要的。
<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>
mu := new(sync.Mutex)
mu.Lock()
</td><td>
var mu sync.Mutex
mu.Lock()
</td></tr>
</tbody></table>
如果你使用結構體指針,mutex 可以非指針形式作為結構體的組成字段,或者更好的方式是直接嵌入到結構體中。
如果是私有結構體類型或是要實現 Mutex 接口的類型,我們可以使用嵌入 mutex 的方法:
<table>
<tbody>
<tr><td>
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]
}
</td><td>
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]
}
</td></tr>
</tr>
<tr>
<td>為私有類型或需要實現互斥接口的類型嵌入。</td>
<td>對于導出的類型,請使用專用字段。</td>
</tr>
</tbody></table>
在邊界處拷貝 Slices 和 Maps
slices 和 maps 包含了指向底層數據的指針,因此在需要復制它們時要特別注意。
接收 Slices 和 Maps
請記住,當 map 或 slice 作為函數參數傳入時,如果您存儲了對它們的引用,則用戶可以對其進行修改。
<table>
<thead><tr><th>Bad</th> <th>Good</th></tr></thead>
<tbody>
<tr>
<td>
func (d *Driver) SetTrips(trips []Trip) {
d.trips = trips
}
trips := ...
d1.SetTrips(trips)
// 你是要修改 d1.trips 嗎?
trips[0] = ...
</td>
<td>
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] = ...
</td>
</tr>
</tbody>
</table>
返回 slices 或 maps
同樣,請注意用戶對暴露內部狀態(tài)的 map 或 slice 的修改。
<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>
type Stats struct {
mu sync.Mutex
counters map[string]int
}
// Snapshot 返回當前狀態(tài)。
func (s *Stats) Snapshot() map[string]int {
s.mu.Lock()
defer s.mu.Unlock()
return s.counters
}
// snapshot 不再受互斥鎖保護
// 因此對 snapshot 的任何訪問都將受到數據競爭的影響
// 影響 stats.counters
snapshot := stats.Snapshot()
</td><td>
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()
</td></tr>
</tbody></table>
使用 defer 釋放資源
使用 defer 釋放資源,諸如文件和鎖。
<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>
p.Lock()
if p.count < 10 {
p.Unlock()
return p.count
}
p.count++
newCount := p.count
p.Unlock()
return newCount
// 當有多個 return 分支時,很容易遺忘 unlock
</td><td>
p.Lock()
defer p.Unlock()
if p.count < 10 {
return p.count
}
p.count++
return p.count
// 更可讀
</td></tr>
</tbody></table>
Defer 的開銷非常小,只有在您可以證明函數執(zhí)行時間處于納秒級的程度時,才應避免這樣做。使用 defer 提升可讀性是值得的,因為使用它們的成本微不足道。尤其適用于那些不僅僅是簡單內存訪問的較大的方法,在這些方法中其他計算的資源消耗遠超過 defer。
Channel 的 size 要么是 1,要么是無緩沖的
channel 通常 size 應為 1 或是無緩沖的。默認情況下,channel 是無緩沖的,其 size 為零。任何其他尺寸都必須經過嚴格的審查。考慮如何確定大小,是什么阻止了 channel 在負載下被填滿并阻止寫入,以及發(fā)生這種情況時發(fā)生了什么。
<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>
// 應該足以滿足任何情況!
c := make(chan int, 64)
</td><td>
// 大?。?
c := make(chan int, 1) // 或者
// 無緩沖 channel,大小為 0
c := make(chan int)
</td></tr>
</tbody></table>
枚舉從 1 開始
在 Go 中引入枚舉的標準方法是聲明一個自定義類型和一個使用了 iota 的 const 組。由于變量的默認值為 0,因此通常應以非零值開頭枚舉。
<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>
type Operation int
const (
Add Operation = iota
Subtract
Multiply
)
// Add=0, Subtract=1, Multiply=2
</td><td>
type Operation int
const (
Add Operation = iota + 1
Subtract
Multiply
)
// Add=1, Subtract=2, Multiply=3
</td></tr>
</tbody></table>
在某些情況下,使用零值是有意義的(枚舉從零開始),例如,當零值是理想的默認行為時。
type LogOutput int
const (
LogToStdout LogOutput = iota
LogToFile
LogToRemote
)
// LogToStdout=0, LogToFile=1, LogToRemote=2
錯誤類型
Go 中有多種聲明錯誤(Error) 的選項:
-
errors.New對于簡單靜態(tài)字符串的錯誤 -
fmt.Errorf用于格式化的錯誤字符串 - 實現
Error()方法的自定義類型 - 用
"pkg/errors".Wrap的 Wrapped errors
返回錯誤時,請考慮以下因素以確定最佳選擇:
這是一個不需要額外信息的簡單錯誤嗎?如果是這樣,
errors.New足夠了。客戶需要檢測并處理此錯誤嗎?如果是這樣,則應使用自定義類型并實現該
Error()方法。您是否正在傳播下游函數返回的錯誤?如果是這樣,請查看本文后面有關錯誤包裝 section on error wrapping 部分的內容。
否則
fmt.Errorf就可以了。
如果客戶端需要檢測錯誤,并且您已使用創(chuàng)建了一個簡單的錯誤 errors.New,請使用一個錯誤變量。
<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>
// 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")
}
}
}
</td><td>
// 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")
}
}
</td></tr>
</tbody></table>
如果您有可能需要客戶端檢測的錯誤,并且想向其中添加更多信息(例如,它不是靜態(tài)字符串),則應使用自定義類型。
<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>
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")
}
}
}
</td><td>
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")
}
}
}
</td></tr>
</tbody></table>
直接導出自定義錯誤類型時要小心,因為它們已成為程序包公共 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”之類的短語來保持上下文簡潔,這些短語會陳述明顯的內容,并隨著錯誤在堆棧中的滲透而逐漸堆積:
<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>
s, err := store.New()
if err != nil {
return fmt.Errorf(
"failed to create new store: %s", err)
}
</td><td>
s, err := store.New()
if err != nil {
return fmt.Errorf(
"new store: %s", err)
}
<tr><td>
failed to x: failed to y: failed to create new store: the error
</td><td>
x: y: new store: the error
</td></tr>
</tbody></table>
但是,一旦將錯誤發(fā)送到另一個系統(tǒng),就應該明確消息是錯誤消息(例如使用err標記,或在日志中以”Failed”為前綴)。
另請參見 Don't just check errors, handle them gracefully. 不要只是檢查錯誤,要優(yōu)雅地處理錯誤
處理類型斷言失敗
type assertion 的單個返回值形式針對不正確的類型將產生 panic。因此,請始終使用“comma ok”的慣用法。
<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>
t := i.(string)
</td><td>
t, ok := i.(string)
if !ok {
// 優(yōu)雅地處理錯誤
}
</td></tr>
</tbody></table>
不要 panic
在生產環(huán)境中運行的代碼必須避免出現 panic。panic 是 cascading failures 級聯(lián)失敗的主要根源 。如果發(fā)生錯誤,該函數必須返回錯誤,并允許調用方決定如何處理它。
<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>
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])
}
</td><td>
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)
}
}
</td></tr>
</tbody></table>
panic/recover 不是錯誤處理策略。僅當發(fā)生不可恢復的事情(例如:nil 引用)時,程序才必須 panic。程序初始化是一個例外:程序啟動時應使程序中止的不良情況可能會引起 panic。
var _statusTemplate = template.Must(template.New("name").Parse("_statusHTML"))
即使在測試代碼中,也優(yōu)先使用t.Fatal或者t.FailNow而不是 panic 來確保失敗被標記。
<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>
// func TestFoo(t *testing.T)
f, err := ioutil.TempFile("", "test")
if err != nil {
panic("failed to set up test")
}
</td><td>
// func TestFoo(t *testing.T)
f, err := ioutil.TempFile("", "test")
if err != nil {
t.Fatal("failed to set up test")
}
</td></tr>
</tbody></table>
使用 go.uber.org/atomic
使用 sync/atomic 包的原子操作對原始類型 (int32, int64等)進行操作,因為很容易忘記使用原子操作來讀取或修改變量。
go.uber.org/atomic 通過隱藏基礎類型為這些操作增加了類型安全性。此外,它包括一個方便的atomic.Bool類型。
<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>
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!
}
</td><td>
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()
}
</td></tr>
</tbody></table>
性能
性能方面的特定準則只適用于高頻場景。
優(yōu)先使用 strconv 而不是 fmt
將原語轉換為字符串或從字符串轉換時,strconv速度比fmt快。
<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>
for i := 0; i < b.N; i++ {
s := fmt.Sprint(rand.Int())
}
</td><td>
for i := 0; i < b.N; i++ {
s := strconv.Itoa(rand.Int())
}
</td></tr>
<tr><td>
BenchmarkFmtSprint-4 143 ns/op 2 allocs/op
</td><td>
BenchmarkStrconv-4 64.2 ns/op 1 allocs/op
</td></tr>
</tbody></table>
避免字符串到字節(jié)的轉換
不要反復從固定字符串創(chuàng)建字節(jié) slice。相反,請執(zhí)行一次轉換并捕獲結果。
<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>
for i := 0; i < b.N; i++ {
w.Write([]byte("Hello world"))
}
</td><td>
data := []byte("Hello world")
for i := 0; i < b.N; i++ {
w.Write(data)
}
</tr>
<tr><td>
BenchmarkBad-4 50000000 22.2 ns/op
</td><td>
BenchmarkGood-4 500000000 3.25 ns/op
</td></tr>
</tbody></table>
盡量初始化時指定 Map 容量
在盡可能的情況下,在使用 make() 初始化的時候提供容量信息
make(map[T1]T2, hint)
為 make() 提供容量信息(hint)嘗試在初始化時調整 map 大小,
這減少了在將元素添加到 map 時增長和分配的開銷。
注意,map 不能保證分配 hint 個容量。因此,即使提供了容量,添加元素仍然可以進行分配。
<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>
m := make(map[string]os.FileInfo)
files, _ := ioutil.ReadDir("./files")
for _, f := range files {
m[f.Name()] = f
}
</td><td>
files, _ := ioutil.ReadDir("./files")
m := make(map[string]os.FileInfo, len(files))
for _, f := range files {
m[f.Name()] = f
}
</td></tr>
<tr><td>
m 是在沒有大小提示的情況下創(chuàng)建的; 在運行時可能會有更多分配。
</td><td>
m 是有大小提示創(chuàng)建的;在運行時可能會有更少的分配。
</td></tr>
</tbody></table>
規(guī)范
一致性
本文中概述的一些標準都是客觀性的評估,是根據場景、上下文、或者主觀性的判斷;
但是最重要的是,保持一致.
一致性的代碼更容易維護、是更合理的、需要更少的學習成本、并且隨著新的約定出現或者出現錯誤后更容易遷移、更新、修復 bug
相反,一個單一的代碼庫會導致維護成本開銷、不確定性和認知偏差。所有這些都會直接導致速度降低、
代碼審查痛苦、而且增加 bug 數量
將這些標準應用于代碼庫時,建議在 package(或更大)級別進行更改,子包級別的應用程序通過將多個樣式引入到同一代碼中,違反了上述關注點。
相似的聲明放在一組
Go 語言支持將相似的聲明放在一個組內。
<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>
import "a"
import "b"
</td><td>
import (
"a"
"b"
)
</td></tr>
</tbody></table>
這同樣適用于常量、變量和類型聲明:
<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>
const a = 1
const b = 2
var a = 1
var b = 2
type Area float64
type Volume float64
</td><td>
const (
a = 1
b = 2
)
var (
a = 1
b = 2
)
type (
Area float64
Volume float64
)
</td></tr>
</tbody></table>
僅將相關的聲明放在一組。不要將不相關的聲明放在一組。
<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>
type Operation int
const (
Add Operation = iota + 1
Subtract
Multiply
ENV_VAR = "MY_ENV"
)
</td><td>
type Operation int
const (
Add Operation = iota + 1
Subtract
Multiply
)
const ENV_VAR = "MY_ENV"
</td></tr>
</tbody></table>
分組使用的位置沒有限制,例如:你可以在函數內部使用它們:
<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>
func f() string {
var red = color.New(0xff0000)
var green = color.New(0x00ff00)
var blue = color.New(0x0000ff)
...
}
</td><td>
func f() string {
var (
red = color.New(0xff0000)
green = color.New(0x00ff00)
blue = color.New(0x0000ff)
)
...
}
</td></tr>
</tbody></table>
import 分組
導入應該分為兩組:
- 標準庫
- 其他庫
默認情況下,這是 goimports 應用的分組。
<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>
import (
"fmt"
"os"
"go.uber.org/atomic"
"golang.org/x/sync/errgroup"
)
</td><td>
import (
"fmt"
"os"
"go.uber.org/atomic"
"golang.org/x/sync/errgroup"
)
</td></tr>
</tbody></table>
包名
當命名包時,請按下面規(guī)則選擇一個名稱:
- 全部小寫。沒有大寫或下劃線。
- 大多數使用命名導入的情況下,不需要重命名。
- 簡短而簡潔。請記住,在每個使用的地方都完整標識了該名稱。
- 不用復數。例如
net/url,而不是net/urls。 - 不要用“common”,“util”,“shared”或“l(fā)ib”。這些是不好的,信息量不足的名稱。
另請參閱 Package Names 和 Go 包樣式指南.
函數名
我們遵循 Go 社區(qū)關于使用 MixedCaps 作為函數名 的約定。有一個例外,為了對相關的測試用例進行分組,函數名可能包含下劃線,如:TestMyFunction_WhatIsBeingTested.
導入別名
如果程序包名稱與導入路徑的最后一個元素不匹配,則必須使用導入別名。
import (
"net/http"
client "example.com/client-go"
trace "example.com/trace/v2"
)
在所有其他情況下,除非導入之間有直接沖突,否則應避免導入別名。
<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>
import (
"fmt"
"os"
nettrace "golang.net/x/trace"
)
</td><td>
import (
"fmt"
"os"
"runtime/trace"
nettrace "golang.net/x/trace"
)
</td></tr>
</tbody></table>
函數分組與順序
- 函數應按粗略的調用順序排序。
- 同一文件中的函數應按接收者分組。
因此,導出的函數應先出現在文件中,放在struct, const, var定義的后面。
在定義類型之后,但在接收者的其余方法之前,可能會出現一個 newXYZ()/NewXYZ()
由于函數是按接收者分組的,因此普通工具函數應在文件末尾出現。
<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>
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{}
}
</td><td>
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 {...}
</td></tr>
</tbody></table>
減少嵌套
代碼應通過盡可能先處理錯誤情況/特殊情況并盡早返回或繼續(xù)循環(huán)來減少嵌套。減少嵌套多個級別的代碼的代碼量。
<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>
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)
}
}
</td><td>
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()
}
</td></tr>
</tbody></table>
不必要的 else
如果在 if 的兩個分支中都設置了變量,則可以將其替換為單個 if。
<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>
var a int
if b {
a = 100
} else {
a = 10
}
</td><td>
a := 10
if b {
a = 100
}
</td></tr>
</tbody></table>
頂層變量聲明
在頂層,使用標準var關鍵字。請勿指定類型,除非它與表達式的類型不同。
<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>
var _s string = F()
func F() string { return "A" }
</td><td>
var _s = F()
// 由于 F 已經明確了返回一個字符串類型,因此我們沒有必要顯式指定_s 的類型
// 還是那種類型
func F() string { return "A" }
</td></tr>
</tbody></table>
如果表達式的類型與所需的類型不完全匹配,請指定類型。
type myError struct{}
func (myError) Error() string { return "error" }
func F() myError { return myError{} }
var _e error = F()
// F 返回一個 myError 類型的實例,但是我們要 error 類型
對于未導出的頂層常量和變量,使用_作為前綴
在未導出的頂級vars和consts, 前面加上前綴_,以使它們在使用時明確表示它們是全局符號。
例外:未導出的錯誤值,應以err開頭。
基本依據:頂級變量和常量具有包范圍作用域。使用通用名稱可能很容易在其他文件中意外使用錯誤的值。
<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>
// 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.
}
</td><td>
// foo.go
const (
_defaultPort = 8080
_defaultUser = "user"
)
</td></tr>
</tbody></table>
結構體中的嵌入
嵌入式類型(例如 mutex)應位于結構體內的字段列表的頂部,并且必須有一個空行將嵌入式字段與常規(guī)字段分隔開。
<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>
type Client struct {
version int
http.Client
}
</td><td>
type Client struct {
http.Client
version int
}
</td></tr>
</tbody></table>
使用字段名初始化結構體
初始化結構體時,幾乎始終應該指定字段名稱?,F在由 go vet 強制執(zhí)行。
<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>
k := User{"John", "Doe", true}
</td><td>
k := User{
FirstName: "John",
LastName: "Doe",
Admin: true,
}
</td></tr>
</tbody></table>
例外:如果有 3 個或更少的字段,則可以在測試表中省略字段名稱。
tests := []struct{
op Operation
want string
}{
{Add, "add"},
{Subtract, "subtract"},
}
本地變量聲明
如果將變量明確設置為某個值,則應使用短變量聲明形式 (:=)。
<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>
var s = "foo"
</td><td>
s := "foo"
</td></tr>
</tbody></table>
但是,在某些情況下,var 使用關鍵字時默認值會更清晰。例如,聲明空切片。
<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>
func f(list []int) {
filtered := []int{}
for _, v := range list {
if v > 10 {
filtered = append(filtered, v)
}
}
}
</td><td>
func f(list []int) {
var filtered []int
for _, v := range list {
if v > 10 {
filtered = append(filtered, v)
}
}
}
</td></tr>
</tbody></table>
nil 是一個有效的 slice
nil 是一個有效的長度為 0 的 slice,這意味著,
-
您不應明確返回長度為零的切片。應該返回
nil來代替。<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>if x == "" { return []int{} }</td><td>
if x == "" { return nil }</td></tr>
</tbody></table> -
要檢查切片是否為空,請始終使用
len(s) == 0。而非nil。<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>func isEmpty(s []string) bool { return s == nil }</td><td>
func isEmpty(s []string) bool { return len(s) == 0 }</td></tr>
</tbody></table> -
零值切片(用
var聲明的切片)可立即使用,無需調用make()創(chuàng)建。<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>nums := []int{} // or, nums := make([]int) if add1 { nums = append(nums, 1) } if add2 { nums = append(nums, 2) }</td><td>
var nums []int if add1 { nums = append(nums, 1) } if add2 { nums = append(nums, 2) }</td></tr>
</tbody></table>
小變量作用域
如果有可能,盡量縮小變量作用范圍。除非它與 減少嵌套的規(guī)則沖突。
<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>
err := ioutil.WriteFile(name, data, 0644)
if err != nil {
return err
}
</td><td>
if err := ioutil.WriteFile(name, data, 0644); err != nil {
return err
}
</td></tr>
</tbody></table>
如果需要在 if 之外使用函數調用的結果,則不應嘗試縮小范圍。
<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>
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
}
</td><td>
data, err := ioutil.ReadFile(name)
if err != nil {
return err
}
if err := cfg.Decode(data); err != nil {
return err
}
fmt.Println(cfg)
return nil
</td></tr>
</tbody></table>
避免參數語義不明確(Avoid Naked Parameters)
函數調用中的意義不明確的參數可能會損害可讀性。當參數名稱的含義不明顯時,請為參數添加 C 樣式注釋 (/* ... */)
<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>
// func printInfo(name string, isLocal, done bool)
printInfo("foo", true, true)
</td><td>
// func printInfo(name string, isLocal, done bool)
printInfo("foo", true /* isLocal */, true /* done */)
</td></tr>
</tbody></table>
對于上面的示例代碼,還有一種更好的處理方式是將上面的 bool 類型換成自定義類型。將來,該參數可以支持不僅僅局限于兩個狀態(tài)(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 支持使用 原始字符串字面值,也就是 " ` " 來表示原生字符串,在需要轉義的場景下,我們應該盡量使用這種方案來替換。
可以跨越多行并包含引號。使用這些字符串可以避免更難閱讀的手工轉義的字符串。
<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>
wantError := "unknown name:\"test\""
</td><td>
wantError := `unknown error:"test"`
</td></tr>
</tbody></table>
初始化 Struct 引用
在初始化結構引用時,請使用&T{}代替new(T),以使其與結構體初始化一致。
<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>
sval := T{Name: "foo"}
// inconsistent
sptr := new(T)
sptr.Name = "bar"
</td><td>
sval := T{Name: "foo"}
sptr := &T{Name: "bar"}
</td></tr>
</tbody></table>
初始化 Maps
對于空 map 請使用 make(..) 初始化, 并且 map 是通過編程方式填充的。
這使得 map 初始化在表現上不同于聲明,并且它還可以方便地在 make 后添加大小提示。
<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>
var (
// m1 讀寫安全;
// m2 在寫入時會 panic
m1 = map[T1]T2{}
m2 map[T1]T2
)
</td><td>
var (
// m1 讀寫安全;
// m2 在寫入時會 panic
m1 = make(map[T1]T2)
m2 map[T1]T2
)
</td></tr>
<tr><td>
聲明和初始化看起來非常相似的。
</td><td>
聲明和初始化看起來差別非常大。
</td></tr>
</tbody></table>
在盡可能的情況下,請在初始化時提供 map 容量大小,詳細請看 盡量初始化時指定 Map 容量。
另外,如果 map 包含固定的元素列表,則使用 map literals(map 初始化列表) 初始化映射。
<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>
m := make(map[T1]T2, 3)
m[k1] = v1
m[k2] = v2
m[k3] = v3
</td><td>
m := map[T1]T2{
k1: v1,
k2: v2,
k3: v3,
}
</td></tr>
</tbody></table>
基本準則是:在初始化時使用 map 初始化列表 來添加一組固定的元素。否則使用 make (如果可以,請盡量指定 map 容量)。
字符串 string format
如果你為Printf-style 函數聲明格式字符串,請將格式化字符串放在外面,并將其設置為const常量。
這有助于go vet對格式字符串執(zhí)行靜態(tài)分析。
<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>
msg := "unexpected values %v, %v\n"
fmt.Printf(msg, 1, 2)
</td><td>
const msg = "unexpected values %v, %v\n"
fmt.Printf(msg, 1, 2)
</td></tr>
</tbody></table>
命名 Printf 樣式的函數
聲明Printf-style 函數時,請確保go vet可以檢測到它并檢查格式字符串。
這意味著您應盡可能使用預定義的Printf-style 函數名稱。go vet將默認檢查這些。有關更多信息,請參見 Printf 系列。
如果不能使用預定義的名稱,請以 f 結束選擇的名稱:Wrapf,而不是Wrap。go vet可以要求檢查特定的 Printf 樣式名稱,但名稱必須以f結尾。
$ go vet -printfuncs=wrapf,statusf
另請參閱 go vet: Printf family check.
編程模式
表驅動測試
當測試邏輯是重復的時候,通過 subtests 使用 table 驅動的方式編寫 case 代碼看上去會更簡潔。
<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>
// 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)
</td><td>
// 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)
})
}
</td></tr>
</tbody></table>
很明顯,使用 test table 的方式在代碼邏輯擴展的時候,比如新增 test case,都會顯得更加的清晰。
我們遵循這樣的約定:將結構體切片稱為tests。 每個測試用例稱為tt。此外,我們鼓勵使用give和want前綴說明每個測試用例的輸入和輸出值。
tests := []struct{
give string
wantHost string
wantPort string
}{
// ...
}
for _, tt := range tests {
// ...
}
功能選項
功能選項是一種模式,您可以在其中聲明一個不透明 Option 類型,該類型在某些內部結構中記錄信息。您接受這些選項的可變編號,并根據內部結構上的選項記錄的全部信息采取行動。
將此模式用于您需要擴展的構造函數和其他公共 API 中的可選參數,尤其是在這些功能上已經具有三個或更多參數的情況下。
<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>
// 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 */)
</td><td>
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),
)
</td></tr>
</tbody></table>
還可以參考下面資料:
本文由zshipu.com學習筆記或整理或轉載,如有侵權請聯(lián)系,必改之。