Uber 是一家美國硅谷的科技公司,也是 Go 語言的早期 adopter。其開源了不少 golang 項目,諸如被 Gopher 圈熟知的 zap、jaeger 等。2018 年年底 Uber 將內部的 Go 風格規範 開源到 GitHub,通過一年的積累和更新,該規範已經初具規模,並受到廣大 Gopher 的關注。本文是該規範的中文版本。本版本會根據原版實時更新。html
樣式 (style) 是支配咱們代碼的慣例。術語樣式
有點用詞不當,由於這些約定涵蓋的範圍不限於由 gofmt 替咱們處理的源文件格式。git
本指南的目的是經過詳細描述在 Uber 編寫 Go 代碼的注意事項來管理這種複雜性。這些規則的存在是爲了使代碼庫易於管理,同時仍然容許工程師更有效地使用 Go 語言功能。github
該指南最初由 Prashant Varanasi 和 Simon Newton 編寫,目的是使一些同事能快速使用 Go。多年來,該指南已根據其餘人的反饋進行了修改。golang
本文檔記錄了咱們在 Uber 遵循的 Go 代碼中的慣用約定。其中許可能是 Go 的通用準則,而其餘擴展準則依賴於下面外部的指南:shell
全部代碼都應該經過golint
和go vet
的檢查並沒有錯誤。咱們建議您將編輯器設置爲:編程
goimports
golint
和 go vet
檢查錯誤您能夠在如下 Go 編輯器工具支持頁面中找到更爲詳細的信息: github.com/golang/go/w…c#
您幾乎不須要指向接口類型的指針。您應該將接口做爲值進行傳遞,在這樣的傳遞過程當中,實質上傳遞的底層數據仍然能夠是指針。api
接口實質上在底層用兩個字段表示:安全
若是但願接口方法修改基礎數據,則必須使用指針傳遞。bash
使用值接收器的方法既能夠經過值調用,也能夠經過指針調用。
例如,
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 的精彩講解。
零值 sync.Mutex
和 sync.RWMutex
是有效的。因此指向 mutex 的指針基本是沒必要要的。
Bad | Good |
---|---|
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 包含了指向底層數據的指針,所以在須要複製它們時要特別注意。
請記住,當 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] = ...
複製代碼 |
一樣,請注意用戶對暴露內部狀態的 map 或 slice 的修改。
Bad | Good |
---|---|
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 釋放資源,諸如文件和鎖。
Bad | Good |
---|---|
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 爲零。任何其餘尺寸都必須通過嚴格的審查。考慮如何肯定大小,是什麼阻止了 channel 在負載下被填滿並阻止寫入,以及發生這種狀況時發生了什麼。
Bad | Good |
---|---|
// 應該足以知足任何狀況!
c := make(chan int, 64)
複製代碼 |
// 大小:1
c := make(chan int, 1) // 或者
// 無緩衝 channel,大小爲 0
c := make(chan int)
複製代碼 |
在 Go 中引入枚舉的標準方法是聲明一個自定義類型和一個使用了 iota 的 const 組。因爲變量的默認值爲 0,所以一般應以非零值開頭枚舉。
Bad | Good |
---|---|
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
,請使用一個錯誤變量。
Bad | Good |
---|---|
// 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")
}
}
複製代碼 |
若是您有可能須要客戶端檢測的錯誤,而且想向其中添加更多信息(例如,它不是靜態字符串),則應使用自定義類型。
Bad | Good |
---|---|
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")
}
}
複製代碼
一個(函數/方法)調用失敗時,有三種主要的錯誤傳播方式:
若是沒有要添加的其餘上下文,而且您想要維護原始錯誤類型,則返回原始錯誤。
添加上下文,使用 "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」之類的短語來保持上下文簡潔,這些短語會陳述明顯的內容,並隨着錯誤在堆棧中的滲透而逐漸堆積:
Bad | Good |
---|---|
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」的慣用法。
Bad | Good |
---|---|
t := i.(string)
複製代碼 |
t, ok := i.(string)
if !ok {
// 優雅地處理錯誤
}
複製代碼 |
在生產環境中運行的代碼必須避免出現 panic。panic 是 cascading failures 級聯失敗的主要根源 。若是發生錯誤,該函數必須返回錯誤,並容許調用方決定如何處理它。
Bad | Good |
---|---|
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 來確保失敗被標記。
Bad | Good |
---|---|
// 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")
}
複製代碼 |
使用 sync/atomic 包的原子操做對原始類型 (int32
, int64
等)進行操做,由於很容易忘記使用原子操做來讀取或修改變量。
go.uber.org/atomic 經過隱藏基礎類型爲這些操做增長了類型安全性。此外,它包括一個方便的atomic.Bool
類型。
Bad | Good |
---|---|
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
快。
Bad | Good |
---|---|
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。相反,請執行一次轉換並捕獲結果。
Bad | Good |
---|---|
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
複製代碼 |
在儘量的狀況下,在使用 make()
初始化的時候提供容量信息
make(map[T1]T2, hint)
複製代碼
爲 make()
提供容量信息(hint)嘗試在初始化時調整 map 大小, 這減小了在將元素添加到 map 時增加和分配的開銷。 注意,map 不能保證分配 hint 個容量。所以,即便提供了容量,添加元素仍然能夠進行分配。
Bad | Good |
---|---|
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
}
複製代碼 |
|
|
本文中概述的一些標準都是客觀性的評估,是根據場景、上下文、或者主觀性的判斷;
可是最重要的是,保持一致.
一致性的代碼更容易維護、是更合理的、須要更少的學習成本、而且隨着新的約定出現或者出現錯誤後更容易遷移、更新、修復 bug
相反,一個單一的代碼庫會致使維護成本開銷、不肯定性和認知誤差。全部這些都會直接致使速度下降、 代碼審查痛苦、並且增長 bug 數量
將這些標準應用於代碼庫時,建議在 package(或更大)級別進行更改,子包級別的應用程序經過將多個樣式引入到同一代碼中,違反了上述關注點。
Go 語言支持將類似的聲明放在一個組內。
Bad | Good |
---|---|
import "a"
import "b"
複製代碼 |
import (
"a"
"b"
)
複製代碼 |
這一樣適用於常量、變量和類型聲明:
Bad | Good |
---|---|
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
)
複製代碼 |
僅將相關的聲明放在一組。不要將不相關的聲明放在一組。
Bad | Good |
---|---|
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"
複製代碼 |
分組使用的位置沒有限制,例如:你能夠在函數內部使用它們:
Bad | Good |
---|---|
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)
)
...
}
複製代碼 |
導入應該分爲兩組:
默認狀況下,這是 goimports 應用的分組。
Bad | Good |
---|---|
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
。另請參閱 Package Names 和 Go 包樣式指南.
咱們遵循 Go 社區關於使用 MixedCaps 做爲函數名 的約定。有一個例外,爲了對相關的測試用例進行分組,函數名可能包含下劃線,如:TestMyFunction_WhatIsBeingTested
.
若是程序包名稱與導入路徑的最後一個元素不匹配,則必須使用導入別名。
import (
"net/http"
client "example.com/client-go"
trace "example.com/trace/v2"
)
複製代碼
在全部其餘狀況下,除非導入之間有直接衝突,不然應避免導入別名。
Bad | Good |
---|---|
import (
"fmt"
"os"
nettrace "golang.net/x/trace"
)
複製代碼 |
import (
"fmt"
"os"
"runtime/trace"
nettrace "golang.net/x/trace"
)
複製代碼 |
所以,導出的函數應先出如今文件中,放在struct
, const
, var
定義的後面。
在定義類型以後,但在接收者的其他方法以前,可能會出現一個 newXYZ()
/NewXYZ()
因爲函數是按接收者分組的,所以普通工具函數應在文件末尾出現。
Bad | Good |
---|---|
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 {...}
複製代碼 |
代碼應經過儘量先處理錯誤狀況/特殊狀況並儘早返回或繼續循環來減小嵌套。減小嵌套多個級別的代碼的代碼量。
Bad | Good |
---|---|
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()
}
複製代碼 |
若是在 if 的兩個分支中都設置了變量,則能夠將其替換爲單個 if。
Bad | Good |
---|---|
var a int
if b {
a = 100
} else {
a = 10
}
複製代碼 |
a := 10
if b {
a = 100
}
複製代碼 |
在頂層,使用標準var
關鍵字。請勿指定類型,除非它與表達式的類型不一樣。
Bad | Good |
---|---|
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 類型
複製代碼
在未導出的頂級vars
和consts
, 前面加上前綴_,以使它們在使用時明確表示它們是全局符號。
例外:未導出的錯誤值,應以err
開頭。
基本依據:頂級變量和常量具備包範圍做用域。使用通用名稱可能很容易在其餘文件中意外使用錯誤的值。
Bad | Good |
---|---|
// 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)應位於結構體內的字段列表的頂部,而且必須有一個空行將嵌入式字段與常規字段分隔開。
Bad | Good |
---|---|
type Client struct {
version int
http.Client
}
複製代碼 |
type Client struct {
http.Client
version int
}
複製代碼 |
初始化結構體時,幾乎始終應該指定字段名稱。如今由 go vet
強制執行。
Bad | Good |
---|---|
k := User{"John", "Doe", true}
複製代碼 |
k := User{
FirstName: "John",
LastName: "Doe",
Admin: true,
}
複製代碼 |
例外:若是有 3 個或更少的字段,則能夠在測試表中省略字段名稱。
tests := []struct{
op Operation
want string
}{
{Add, "add"},
{Subtract, "subtract"},
}
複製代碼
若是將變量明確設置爲某個值,則應使用短變量聲明形式 (:=
)。
Bad | Good |
---|---|
var s = "foo"
複製代碼 |
s := "foo"
複製代碼 |
可是,在某些狀況下,var
使用關鍵字時默認值會更清晰。例如,聲明空切片。
Bad | Good |
---|---|
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
是一個有效的長度爲 0 的 slice,這意味着,
您不該明確返回長度爲零的切片。應該返回nil
來代替。
Bad | Good |
---|---|
if x == "" {
return []int{}
}
複製代碼 |
if x == "" {
return nil
}
複製代碼 |
要檢查切片是否爲空,請始終使用len(s) == 0
。而非 nil
。
Bad | Good |
---|---|
func isEmpty(s []string) bool {
return s == nil
}
複製代碼 |
func isEmpty(s []string) bool {
return len(s) == 0
}
複製代碼 |
零值切片(用var
聲明的切片)可當即使用,無需調用make()
建立。
Bad | Good |
---|---|
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)
}
複製代碼 |
若是有可能,儘可能縮小變量做用範圍。除非它與 減小嵌套的規則衝突。
Bad | Good |
---|---|
err := ioutil.WriteFile(name, data, 0644)
if err != nil {
return err
}
複製代碼 |
if err := ioutil.WriteFile(name, data, 0644); err != nil {
return err
}
複製代碼 |
若是須要在 if 以外使用函數調用的結果,則不該嘗試縮小範圍。
Bad | Good |
---|---|
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
複製代碼 |
函數調用中的意義不明確的參數
可能會損害可讀性。當參數名稱的含義不明顯時,請爲參數添加 C 樣式註釋 (/* ... */
)
Bad | Good |
---|---|
// 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 支持使用 原始字符串字面值,也就是 " ` " 來表示原生字符串,在須要轉義的場景下,咱們應該儘可能使用這種方案來替換。
能夠跨越多行幷包含引號。使用這些字符串能夠避免更難閱讀的手工轉義的字符串。
Bad | Good |
---|---|
wantError := "unknown name:\"test\""
複製代碼 |
wantError := `unknown error:"test"`
複製代碼 |
在初始化結構引用時,請使用&T{}
代替new(T)
,以使其與結構體初始化一致。
Bad | Good |
---|---|
sval := T{Name: "foo"}
// inconsistent
sptr := new(T)
sptr.Name = "bar"
複製代碼 |
sval := T{Name: "foo"}
sptr := &T{Name: "bar"}
複製代碼 |
對於空 map 請使用 make(..)
初始化, 而且 map 是經過編程方式填充的。 這使得 map 初始化在表現上不一樣於聲明,而且它還能夠方便地在 make 後添加大小提示。
Bad | Good |
---|---|
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 初始化列表) 初始化映射。
Bad | Good |
---|---|
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 容量)。
若是你爲Printf
-style 函數聲明格式字符串,請將格式化字符串放在外面,並將其設置爲const
常量。
這有助於go vet
對格式字符串執行靜態分析。
Bad | Good |
---|---|
msg := "unexpected values %v, %v\n"
fmt.Printf(msg, 1, 2)
複製代碼 |
const msg = "unexpected values %v, %v\n"
fmt.Printf(msg, 1, 2)
複製代碼 |
聲明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 代碼看上去會更簡潔。
Bad | Good |
---|---|
// 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
。此外,咱們鼓勵使用give
和want
前綴說明每一個測試用例的輸入和輸出值。
tests := []struct{
give string
wantHost string
wantPort string
}{
// ...
}
for _, tt := range tests {
// ...
}
複製代碼
功能選項是一種模式,您能夠在其中聲明一個不透明 Option 類型,該類型在某些內部結構中記錄信息。您接受這些選項的可變編號,並根據內部結構上的選項記錄的所有信息採起行動。
將此模式用於您須要擴展的構造函數和其餘公共 API 中的可選參數,尤爲是在這些功能上已經具備三個或更多參數的狀況下。
Bad | Good |
---|---|
// 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),
)
複製代碼 |
還能夠參考下面資料:
本文由zshipu.com學習筆記或整理或轉載,若有侵權請聯繫,必改之。