Go Code Review Comments 譯文

持續更新中…
原文最新連接
https://github.com/golang/go/wiki/CodeReviewComments/5a40ba36d388ff1b8b2dd4c1c3fe820b8313152f
Github譯文連接
https://github.com/wddpct/articles/issues/8html

對於剛開始學習和使用 Go 的新手來講,有這麼幾個資源值得關注。node

  1. A Tour of Go
  2. How to Write Go Code
  3. Frequently Asked Questions (FAQ)
  4. The Go Blog Index
  5. The Go Wiki
  6. Effective Go
  7. Go Code Review Comments
  8. ……

Effective Go 和 Go Code Review Comments 中介紹了不少有助於編寫優雅,高效的 Go 代碼的指導性原則,前者能夠認爲是官方權威指南,然後者則能夠算是對前者的補充,原文託管在 Github 上,每月都會作部分 fix,而網上的中文版大多都是歷史版本,沒法及時更新,因此想由本身在業餘時作些額外的工做,水平通常,能力有限,少部分難以翻譯的詞句將附上原文或給出意譯內容。如下是翻譯正文。git


Go Code Review Comments

當前頁面收集了在 Go 代碼審覈期間的常見意見,以便一個詳細說明能被快速參考。這是一個常見錯誤的清單,而非綜合性的風格指南。程序員

你也能夠將它做爲是 Effective Go 的補充。github

請在編輯這個頁面前先討論這個變動,就算是一個很小的變動。畢竟許多人都有本身的想法,而這裏並非戰場。golang

Gofmt

在 Go 代碼上運行 gofmt 以自動修復大多數的機械性風格問題。幾乎全部不正規的 Go 代碼都在使用gofmt。本文檔的剩餘部分涉及非機械性風格問題。編程

另外一種方法是使用 goimports,這是gofmt的超集,gofmt可根據須要額外添加(和刪除)導入行。json

Comment Sentences

參見 https://golang.org/doc/effective_go.html#commentary。註釋文檔聲明應該是完整的句子,即便這看起來有些多餘。這種方式使註釋在提取到 godoc 文檔時格式良好。註釋應以所描述事物的名稱開頭,並以句點結束:swift

// Request represents a request to run a command.
type Request struct { ...

// Encode writes the JSON encoding of req to w.
func Encode(w io.Writer, req *Request) { ...

請注意除了句點以外還有其餘符號能夠做爲句子的有效結尾(但至少也應該是!,?)。除此以外,還有許多工具使用註釋來標記類型和方法(如 easyjson:json 和 golint 的 MATCH)。這使得這條規則難以形式化。api

Contexts

A function that is never request-specific may use context.Background(), but err on the side of passing a Context even if you think you don't need to. The default case is to pass a Context; only use context.Background() directly if you have a good reason why the alternative is a mistake.

context.Context 類型的值包含跨 API 和進程邊界的安全憑證,跟蹤信息,截止時間和取消信號。好比傳入 RPC 請求和 HTTP 請求一直到傳出相關請求,Go 程序在整個過程的函數調用鏈中顯式地傳遞 Context。

大多數使用 Context 的函數都應該接受 Context 做爲函數的第一個參數:

func F(ctx context.Context, /* other arguments */) {}

從不特定於請求(request-specific)的函數可使用 context.Background() 獲取 Context,並將 err 與 Context 同時傳遞,即便你認爲不須要。默認狀況下只傳遞 Context ;只在你有充分的理由認爲這是錯誤的,才能直接使用context.Background()。

原文: A function that is never request-specific may use context.Background(), but err on the side of passing a Context even if you think you don't need to. The default case is to pass a Context; only use context.Background() directly if you have a good reason why the alternative is a mistake.

不要將 Context 成員添加到某個 struct 類型中;而是將 ctx 參數添加到該類型的方法上。一個例外狀況是當前方法簽名必須與標準庫或第三方庫中的接口方法匹配。

不要在函數簽名中建立自定義 Context 類型或使用除了 Context 之外的接口。

若是要傳遞應用程序數據,請將其放在參數,方法接收器,全局變量中,或者若是它確實應該屬於 Context,則放在 Context 的 Value 屬性中。

全部的 Context 都是不可變的,所以能夠將相同的 ctx 傳遞給多個共享相同截止日期,取消信號,安全憑據,跟蹤等的調用。

Copying

爲避免意外的別名,從另外一個包複製 struct 時要當心。例如,bytes.Buffer 類型包含一個 []byte 的 slice,而且做爲短字符串的優化,slice 能夠引用一個短字節數組。若是複製一個 Buffer,副本中的 slice 可能會對原始數組進行別名操做,從而致使後續方法調用產生使人驚訝的效果。

一般,若是 T 類型的方法與其指針類型 *T 相關聯,請不要複製 T 類型的值。

Crypto Rand

不要使用包math/rand來生成密鑰,即便是一次性密鑰。在沒有種子(seed)的狀況下,生成器是徹底能夠被預測的。使用time.Nanoseconds()做爲種子值,熵只有幾位。請使用crypto/rand的 Reader 做爲替代,若是你傾向於使用文本,請輸出成十六進制或 base64 編碼:

import (
    "crypto/rand"
    // "encoding/base64"
    // "encoding/hex"
    "fmt"
)

func Key() string {
    buf := make([]byte, 16)
    _, err := rand.Read(buf)
    if err != nil {
        panic(err)  // out of randomness, should never happen
    }
    return fmt.Sprintf("%x", buf)
    // or hex.EncodeToString(buf)
    // or base64.StdEncoding.EncodeToString(buf)
}

Declaring Empty Slices

當聲明一個空 slice 時,傾向於用

var t []string

代替

t := []string{}

前者聲明瞭一個 nil slice 值,然後者聲明瞭一個非 nil 可是零長度的 slice。二者在功能上等同,len 和 cap 均爲零,而 nil slice 是首選的風格。

請注意,在部分場景下,首選非零但零長度的切片,例如編碼 JSON 對象時(前者編碼爲 null,然後者則能夠正確編碼爲 JSON array[])。

在設計 interface 時,避免區分 nil slice 和 非 nil,零長度的 slice,由於這會致使細微的編程錯誤。

有關 Go 中對於 nil 的更多討論,請參閱 Francesc Campoy 的演講 Understanding Nil

Doc Comments

全部的頂級導出的名稱都應該有 doc 註釋,重要的未導出類型或函數聲明也應如此。有關注釋約束的更多信息,請參閱 https://golang.org/doc/effective_go.html#commentary

Don't Panic

請參閱 https://golang.org/doc/effective_go.html#errors。不要將 panic 用於正常的錯誤處理。使用 error 和多返回值。

Error Strings

Error strings should not be capitalized (unless beginning with proper nouns or acronyms) or end with punctuation, since they are usually printed following other context. That is, use fmt.Errorf("something bad") not fmt.Errorf("Something bad"), so that log.Printf("Reading %s: %v", filename, err) formats without a spurious capital letter mid-message. This does not apply to logging, which is implicitly line-oriented and not combined inside other messages.

錯誤信息字符串不該大寫(除非以專有名詞或首字母縮略詞開頭)或以標點符號結尾,由於它們一般是在其餘上下文後打印的。即便用fmt.Errorf("something bad")而不要使用fmt.Errorf("Something bad"),所以log.Printf("Reading %s: %v", filename, err)的格式中將不會出現額外的大寫字母。不然這將不適用於日誌記錄,由於它是隱式的面向行,而不是在其餘消息中組合。

Examples

When adding a new package, include examples of intended usage: a runnable Example, or a simple test demonstrating a complete call sequence.

Read more about testable Example() functions.

添加新包時,請包含預期用法的示例:可運行的示例,或是演示完整調用鏈的簡單測試。

閱讀有關 testable Example() functions 的更多信息。

Goroutine Lifetimes

當你生成 goroutines 時,要清楚它們什麼時候或是否會退出。

經過阻塞 channel 的發送或接收可能會引發 goroutines 的內存泄漏:即便被阻塞的 channel 沒法訪問,垃圾收集器也不會終止 goroutine。

即便 goroutines 沒有泄漏,當它們再也不須要時卻仍然將其留在內存中會致使其餘細微且難以診斷的問題。往已經關閉的 channel 發送數據將會引起 panic。在「結果不被須要以後」修改仍在使用的輸入仍然可能致使數據競爭。而且將 goroutines 留在內存中任意長時間將會致使不可預測的內存使用。

請儘可能讓併發代碼足夠簡單,從而更容易地確認 goroutine 的生命週期。若是這不可行,請記錄 goroutines 退出的時間和緣由。

Handle Errors

請參閱 https://golang.org/doc/effective_go.html#errors。不要使用 _ 變量丟棄 error。若是函數返回 error,請檢查它以確保函數成功。處理 error,返回 error,或者在真正特殊的狀況下使用 panic。

Imports

Avoid renaming imports except to avoid a name collision; good package names should not require renaming. In the event of collision, prefer to rename the most local or project-specific import.

避免包重命名導入,防止名稱衝突;好的包名稱不須要重命名。若是發生命名衝突,則更傾向於重命名最接近本地的包或特定於項目的包。

包導入按組進行組織,組與組之間有空行。標準庫包始終位於第一組中。

package main

import (
    "fmt"
    "hash/adler32"
    "os"

    "appengine/foo"
    "appengine/user"

    "github.com/foo/bar"
    "rsc.io/goversion/version"
)

goimports 會爲你作這件事。

Import Dot

部分包因爲循環依賴,不能做爲測試包的一部分進行測試時,以.形式導入它們可能頗有用:

package foo_test

import (
    "bar/testutil" // also imports "foo"
    . "foo"
)

在這種狀況下,測試文件不能位於 foo 包中,由於它使用的 bar/testutil 依賴於 foo 包。因此咱們使用import .形式使得測試文件假裝成 foo 包的一部分,即便它不是。除了這種狀況,不要在程序中使用 import .。它將使程序更難閱讀——由於不清楚如 Quux 這樣的名稱是不是當前包中或導入包中的頂級標識符。

In-Band Errors

在 C 和類 C 語言中,一般使函數返回 -1 或 null 之類的值用來發出錯誤信號或缺乏結果:

// Lookup returns the value for key or "" if there is no mapping for key.
func Lookup(key string) string

// Failing to check a for an in-band error value can lead to bugs:
Parse(Lookup(key))  // returns "parse failure for value" instead of "no value for key"

Go 對多返回值的支持提供了一種更好的解決方案。函數應返回一個附加值以指示其餘返回值是否有效,而不是要求客戶端檢查 in-band 錯誤值。此附加值多是一個 error,或者在不須要解釋時能夠是布爾值。它應該是最終的返回值。

// Lookup returns the value for key or ok=false if there is no mapping for key.
func Lookup(key string) (value string, ok bool)

這能夠防止調用者錯誤地使用返回結果:

Parse(Lookup(key))  // compile-time error

並鼓勵更健壯和可讀性強的代碼:

value, ok := Lookup(key)
if !ok  {
    return fmt.Errorf("no value for %q", key)
}
return Parse(value)

此規則適用於公共導出函數,但對於未導出函數也頗有用。

返回值如 nil,「」,0 和 -1 在他們是函數的有效返回結果時是可接收的,即調用者不須要將它們與其餘值作分別處理。

某些標準庫函數(如 「strings」 包中的函數)會返回 in-band 錯誤值。這大大簡化了字符串操做,代價是須要程序員作更多事。一般,Go 代碼應返回表示錯誤的附加值。

Indent Error Flow

Try to keep the normal code path at a minimal indentation, and indent the error handling, dealing with it first. This improves the readability of the code by permitting visually scanning the normal path quickly. For instance, don't write:

嘗試將正常的代碼路徑保持在最小的縮進處,優先處理錯誤並縮進。經過容許快速可視化掃描正常路徑來提升代碼的可讀性。例如,不要寫:

if err != nil {
    // error handling
} else {
    // normal code
}

相反,書寫如下代碼:

if err != nil {
    // error handling
    return // or continue, etc.
}
// normal code

若是 if 語句具備初始化語句,例如:

if x, err := f(); err != nil {
    // error handling
    return
} else {
    // use x
}

那麼這可能須要將短變量聲明移動到新行:

x, err := f()
if err != nil {
    // error handling
    return
}
// use x

Initialisms

名稱中的單詞是首字母或首字母縮略詞(例如 「URL」 或 「NATO」 )須要具備相同的大小寫規則。例如,「URL」 應顯示爲 「URL」 或 「url」 (如 「urlPony」 或 「URLPony」 ),而不是 「Url」。舉個例子:ServeHTTP 不是 ServeHttp。對於具備多個初始化 「單詞」 的標識符,也應當顯示爲 「xmlHTTPRequest」 或 「XMLHTTPRequest」。

當 「ID」 是 「identifier」 的縮寫時,此規則也適用於 「ID」 ,所以請寫 「appID」 而不是「appId」。

由協議緩衝區編譯器生成的代碼不受此規則的約束。人工編寫的代碼比機器編寫的代碼要保持更高的標準。

Interfaces

Go 接口一般屬於使用 interface 類型值的包,而不是實現這些值的包。實現包應返回具體(一般是指針或結構)類型:這樣一來能夠將新方法添加到實現中,而無需進行大量重構。

不要在 API 的實現者端定義 「for mocking」 接口;相反,設計 API 以即可以使用真實實現的公共 API 進行測試。

在使用接口以前不要定義接口:若是沒有真實的使用示例,很難看出接口是不是必要的,更不用說它應該包含哪些方法。

package consumer  // consumer.go

type Thinger interface { Thing() bool }

func Foo(t Thinger) string { … }
package consumer // consumer_test.go

type fakeThinger struct{ … }
func (t fakeThinger) Thing() bool { … }
…
if Foo(fakeThinger{…}) == "x" { … }
// DO NOT DO IT!!!
package producer

type Thinger interface { Thing() bool }

type defaultThinger struct{ … }
func (t defaultThinger) Thing() bool { … }

func NewThinger() Thinger { return defaultThinger{ … } }

相反,返回一個具體的類型,讓消費者模擬生產者實現。

package producer

type Thinger struct{ … }
func (t Thinger) Thing() bool { … }

func NewThinger() Thinger { return Thinger{ … } }

Line Length

Go代碼中沒有嚴格的行長度限制,但避免使用形成閱讀障礙的長行。相似的,若是長行的可讀性更好,不要爲了縮短行而添加換行符——例如,行組成是重複的。

大多數狀況下,當人們 「不天然地」 自動換行(wrap lines)時(在函數調用或函數聲明的中間,或多或少,好比,雖然有一些例外),若是它們有合理數量的參數而且變量名稱較短時,自動換行將是沒必要要的。長行彷佛與長名稱有關,避免名稱過長有很大幫助。

換句話說,換行是由於你所寫的語義(做爲通常規則)而不是由於行的長度。若是您發現這會產生太長的行,那麼更更名稱或語義,可能也會獲得一個好結果。

實際上,這與關於函數應該有多長的建議徹底相同。沒有 「永遠不會有超過N行的函數」 這樣的規則,可是程序中確定會存在行數太多,功能過於微弱的函數,而解決方案是改變這個函數邊界的位置,而不是執着在行數上。

Mixed Caps

請參閱 https://golang.org/doc/effective_go.html#mixed-caps。即便 Go 中混合大小寫的規則打破了其餘語言的慣例,也是適用的。例如,未導出的常量寫成 maxLength 而不是MaxLength或MAX_LENGTH。

另見當前頁面的 Initialisms 一節。

Named Result Parameters

考慮一下 godoc 中會是什麼樣子。命名結果參數如:

func (n *Node) Parent1() (node *Node) func (n *Node) Parent2() (node *Node, err error)

將會形成口吃現象(stutter); 最好這樣使用:

func (n *Node) Parent1() *Node func (n *Node) Parent2() (*Node, error)

另外一方面,若是函數返回兩個或三個相同類型的參數,或者若是從上下文中不清楚返回結果的含義,那麼在某些上下文中添加命名可能頗有用。可是不要僅僅爲了不在函數內作結果參數的聲明(var 或者 :=)而命名結果參數;這以犧牲沒必要要的 API 冗長性爲代價,換取了一個微小的實現簡潔性。

func (f *Foo) Location() (float64, float64, error)

不如如下代碼清晰:

// Location returns f's latitude and longitude.
// Negative values mean south and west, respectively.
func (f *Foo) Location() (lat, long float64, err error)

若是函數行數較少,那麼非命名結果參數是能夠的。一旦它是一箇中等規模的函數,請明確返回值。推論:僅僅由於它使得可以直接使用預命名返回而命名結果參數是不值得的。文檔的清晰度總比在函數中的一行兩行更重要。

最後,在某些狀況下,您須要命名結果參數,以便在延遲閉包中更改它,這也是能夠的。

Naked Returns

請參閱當前頁面 Named Result Parameters 一節。

Package Comments

與 godoc 呈現的全部註釋同樣,包註釋必須出如今 package 聲明的臨近位置,無空行。

// Package math provides basic constants and mathematical functions.
package math
/* Package template implements data-driven templates for generating textual output such as HTML. .... */
package template

For "package main" comments, other styles of comment are fine after the binary name (and it may be capitalized if it comes first), For example, for a package main in the directory seedgen you could write:

對於 「package main」 註釋,在二進制文件名稱以後可使用其餘樣式的註釋(若是它們放在前面,則能夠大寫),例如,對於你能夠編寫 seedgen 目錄中下的 main 包註釋:

// Binary seedgen ...
package main

或是

// Command seedgen ...
package main

或是

// Program seedgen ...
package main

或是

// The seedgen command ...
package main

或是

// The seedgen program ...
package main

或是

// Seedgen ..
package main

以上是相應示例,它們的合理變體也是能夠接受的。

請注意,以小寫單詞開頭的句子不屬於包註釋的可接受選項,由於註釋是公開可見的,應該用適當的英語書寫,包括將句子的第一個單詞的首字母大寫。當二進制文件名稱是第一個單詞時,即便它與命令行調用的拼寫不嚴格匹配,也須要對其進行大寫。

有關評論約定的更多信息,請參閱https://golang.org/doc/effective_go.html#commentary。

Package Names

包中名稱的全部引用都將使用包名完成,所以您能夠從標識符中省略該名稱。例如,若是有一個 chubby 包,你不該該定義類型名稱爲 ChubbyFile ,不然使用者將寫爲 chubby.ChubbyFile。而是應該命名類型名稱爲 File,使用時將寫爲 chubby.File。避免使用無心義的包名稱,如 util,common,misc,api,types 和 interfaces。有關更多信息,請參閱http://golang.org/doc/effective_go.html#package-names和 http://blog.golang.org/package-names

Pass Values

不要只是爲了節省幾個字節就將指針做爲函數參數傳遞。若是一個函數在整個過程當中只引用它的參數x做爲x,那麼這個參數不該該是一個指針。此常見實例包括將指針傳遞給 string(string)或是指向接口值(*io.Reader)的指針。在這兩種狀況下,值自己都是固定大小,能夠直接傳遞。這個建議不適用於大型 struct ,甚至不適用於可能生長的小型 struct。

Receiver Names

方法接收者的名稱應該反映其身份;一般,其類型的一個或兩個字母縮寫就足夠了(例如「client」的「c」或「cl」)。不要使用通用名稱,例如「me」,「this」或「self」,這是面嚮對象語言的典型標識符,它們更強調方法而不是函數。名稱沒必要像方法論證那樣具備描述性,由於它的做用是顯而易見的,不起任何記錄目的。名稱能夠很是短,由於它幾乎出如今每種類型的每一個方法的每一行上;familiarity admits brevity。使用上也要保持一致:若是你在一個方法中叫將接收器命名爲「c」,那麼在其餘方法中不要把它命名爲「cl」。

Receiver Type

選擇究竟是在方法上使用值接收器仍是使用指針接收器可能會很困難,尤爲是對於 Go 新手程序員。若有疑問,請使用指針接收器,但有時候值接收器是有意義的,一般是出於效率的緣由,例如小的不變結構或基本類型的值。如下是一些有用的指導:

  • 若是接收器是 map,func或 chan,則不要使用指向它們的指針。若是接收器是 slice 而且該方法不從新切片或不從新分配切片,則不要使用指向它的指針。
  • 若是該方法須要改變接收器的值,則接收器必須是指針。
  • 若是接收器是包含 sync.Mutex 或相似同步字段的 struct,則接收器必須是避免複製的指針。
  • 若是接收器是大型結構或數組,則指針接收器更有效。多大才算大?假設它至關於將其包含的全部元素做爲參數傳遞給方法。若是感受太大,那麼對接收器來講也太大了。
  • 函數或方法能夠改變接收器嗎(併發調用或調用某方法時繼續調用相關方法或函數)?在調用方法時,值類型會建立接收器的副本,所以外部更新將不會應用於此接收器。若是必須在原始接收器中看到更改效果,則接收器必須是指針。
  • 若是接收器是 struct,數組或 slice,而且其任何元素是指向可能改變的對象的指針,則更傾向於使用指針接收器,由於它將使讀者更清楚地意圖。
  • 若是接收器是一個小型數組或 struct,那麼它天然是一個值類型(例如,相似於time.Time類型),對於沒有可變字段,沒有指針的類型,或者只是一個簡單的基本類型,如 int 或 string,值接收器是合適的。值接收器能夠減小能夠生成的垃圾量;若是將值做爲參數傳遞給值類型方法,則可使用堆棧上的副本而不須要在堆上進行分配。(編譯器試圖避免這種分配,但它不能老是成功)所以,在沒有進行分析以前,不要選擇值接收器類型。
  • 最後,若有疑問,請使用指針接收器。

Synchronous Functions

相比異步函數更傾向於同步函數——直接返回結果的函數,或是在返回以前已完成全部回調或 channel 操做的函數。

同步函數讓 goroutine 在調用中本地化,可以更容易地推斷其生命週期並避免泄漏和數據競爭。同步函數也更容易測試:調用者能夠傳遞輸入並檢查輸出,而無需輪詢或同步。

若是調用者須要更多的併發性,他們能夠定義和調用單獨的 goroutine 中的函數來輕鬆實現。可是在調用者端刪除沒必要要的併發性是很是困難的——有時是不可能的。

Useful Test Failures

失敗的測試也應該提供有用的消息,說明錯誤,展現輸入內容,實際內容以及預期結果。編寫一堆 assertFoo 幫助程序可能很吸引人,但請確保您的幫助程序能產生有用的錯誤消息。假設調試失敗測試的人不是你,也不是你的團隊。典型的 Go 失敗測試如:

if got != tt.want {
    t.Errorf("Foo(%q) = %d; want %d", tt.in, got, tt.want) // or Fatalf, if test can't test anything more past this point }

請注意,此處的命令是 實際結果!=預期結果,而且錯誤消息也使用該命令格式。然而一些測試框架鼓勵倒寫輸出格式,如 預期結果 != 實際結果,「預期結果爲 0,實際結果爲 x」,等等。可是 Go 沒有這樣作。

若是這看起來像是打了不少字,你可能想寫一個表驅動的測試。

在使用具備不一樣輸入的測試幫助程序時以消除失敗測試歧義的另外一種常見技術是使用不一樣的 TestFoo 函數包裝每一個調用者,而測試名稱也根據對應的輸入命名:

func TestSingleValue(t *testing.T) { testHelper(t, []int{80}) }
func TestNoValues(t *testing.T)    { testHelper(t, []int{}) }

In any case, the onus is on you to fail with a helpful message to whoever's debugging your code in the future.

在任何狀況下,你都有責任向可能會在未來調試你的代碼的開發者提供有用的消息。

Variable Names

Go 中的變量名稱應該短而不是長。對於範圍域中的局部變量尤爲如此。例如爲 line count 定義 c 變量,爲 slice index 定義 i 變量。

基本規則:範圍域中,越晚使用的變量,名稱必須越具備描述性。對於方法接收器,一個或兩個字母就足夠了。諸如循環索引和讀取器(Reader)之類的公共變量能夠是單個字母(i,r)。更多不尋常的事物和全局變量則須要更具描述性的名稱。

相關文章
相關標籤/搜索