Go 程式碼審查建議

yexiaobai發表於2019-02-16

注:該文的原文來自於 go-wiki 為 Go Code Review Comments

Go 程式碼審查建議

該頁收集了 Go 程式碼審查時候的常見意見,以至於一個詳細說明能被快速參考。這是一個常見的錯誤清單,而不是一個風格指南。

你可以看 effective go 作為補充。

請在編輯這個頁面前先討論這個變更,就算是一個很小的變更,許多人都有自己的想法,這裡不是戰場。

gofmt

執行 gofmt 來自動化的解決你程式碼的主要的機械的風格問題,幾乎所有的不正規的 go 程式碼都使用 gofmt。該文件的其餘部分涉及非機械式的風格點。

另外一個替代方案是使用 goimports,它是 gofmt 的父集,額外的新增(和移除)了 import 行。

註釋句子

檢視 http://golang.org/doc/effective_go.html#commentary。註釋文件宣告瞭應該是整句,即使它看起來是有點多餘的。這個方法使得它被提取進 godoc 文件的時候,是非常格式化的。註釋應該開始於這個事物被描述的名字,並在一段時期結束。

// A 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) { ...

等等。

Doc 建議

所有頂級的,匯出的名字都應該有文件註釋,作為 non-trivial unexported type 和功能宣告。看 http://golang.org/doc/effective_go.html#commentary 來獲取更多的註釋約定的更多資訊。

不要使用 Panic

http://golang.org/doc/effective_go.html#errors,不要使用 panic 用於正常的錯誤處理。使用 error 和多個返回值。

Error Strings

Error strings 不應該大寫(除非有專有名詞或是首字母縮小)或是以標點符號結束。因為它們通常在其他上下文中列印。即使用 fmt.Errorf(“something bad”) 而不是 fmt.Errorf(“Something bad”),以至於 log.Print(“Reading %s: %v”, filename, err) 格式沒有一個虛假的大寫字母中間訊息。這個不適用於日誌記錄,這個是絕對面向行的,不被包含在其他資訊中。

注:上面 Errorf 區別是裡面的內容 s 這個 字母一個大寫一個小寫】

處理錯誤

http://golang.org/doc/effective_go.html#errors,不要使用 _ 變數丟棄錯誤,如果一個函式返回一個錯誤,檢查它確保函式式成功的。處理這個錯誤,返回它,或是在真正的異常情況下使用 panic。

Imports

Imports 是以組的形式組織的,在它們之間使用空行,標準包在第一組。

package main

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

    "appengine/user"
    "appengine/foo"

    "code.google.com/p/x/y"
    "github.com/foo/bar"
)

goimports 將幫助你做到這個。

Import 的點號

import 的點號形式對測試是非常有用的,由於迴圈依賴,不能成為被測試的包的一部分。

package foo_test

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

在這個例子中,測試檔案不能在 foo 包中,因為它使用 bar/testutil,它也引入 foo,因此我們使用 `import .` 形式來偽裝成 foo 包的一部分,即使它不是。除了這種情況,不要在你的程式中使用 `import .` 。它會使得你的程式難以閱讀,因為它是難以理解的,一個名字比如 Quux 在當前包種是否是一個頂級的識別符號或是在一個引入包中。

Indent Error Flow

在一個最小的程式碼縮排中嘗試儲存正常的程式碼路徑,和縮排錯誤處理,首先處理錯誤。這樣做提升了程式的可讀性,這個符合視覺的快速掃描習慣。例如,不要這些寫:

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

首字母縮寫

名字的單詞應該是首字母縮寫的(比如,”URL” 或 “NATO”)是一致的情況。例如,”URL” 應該以 “URL” 或是 ”url“ 展現(就像 “urlPony”, 或 “URLPony”),絕不是 Url 。這裡有一個示例:ServeHTTP 而不是 ServeHttp。

這個規則也適用於 “ID”,當它是 “identifier” 短名稱的時候。因此使用 “appID” 代替 “appId”。

被 protocol buffer 編譯器生成的程式碼是脫離了這個規則的。人類寫的程式碼應該比機器寫的程式碼更高標準。

行長度

這在 Go 的程式碼中沒有嚴格的限制,但是為了避免太長的行,同樣的,當他們在更可讀的長度的時候,不應該新增換行符使其更短 — 比如,如果他們是重複的。

建議是通常在包裝前不超過 80 個字元,不是因為它是一個規則,而是因為它在一個可顯示幾百列的編輯器中檢視的時候可讀性更高。人類更適合窄文字相對於一個寬文字。不管怎樣,godoc 應該以更好的方式渲染它。

Mixed Caps

http://golang.org/doc/effective_go.html#mixed-caps,這個甚至當它打破了其他語言的習慣的時候也適用。比如一個未匯出的常量是 maxLength 而不是 MaxLength 或是 MAX_LENGTH。

結果引數命名

考慮下這個在 godoc 應該是看起來像什麼,結果引數命名像:

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

更好的用法是:

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

換句話說,如果一個函式返回了兩個或是三個相同型別的引數,或者如果一個結果的含義通過上下文不清楚,新增命名會非常有用,比如:

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 返回值是好的,如果函式很小。一旦它是一箇中型函式,必須明確你的返回值,必然的結果:僅僅使得你使用 naked 返回值是不知道命名結果引數的。清晰的文件一直是比在你的函式中儲存一行或兩行更重要。

最後,在一些情況下,為了在一個 deferred closure 中改變它,你需要命名一個結果引數。這通常是沒有問題的。

Naked Returns

檢視 CodeReviewComments#Named_Result_Parameters

包建議

Package 建議,就像所有的註釋都應該由 godoc 呈現,必須與包相鄰,而沒有空行:

// 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

檢視 http://golang.org/doc/effective_go.html#commentary 獲取更多關於註釋約定的資訊。

包名

在你的包中的所有的引用的名稱應該使用包名,因此你可以忽略這個名字的識別符號。比如,如果你在包 chubby 中,你不需要鍵入 ChubbyFile,客戶端將寫成 chubby.ChubbyFile。相反,命名該型別檔案,客戶端將寫成 chubby.File,看 http://golang.org/doc/effective_go.html#package-names 獲取更多資訊

傳遞值

不要將指標作為函式引數傳遞只是為了省幾位元組,如果函式引用的引數 x 僅僅至始至終都作為 x,那麼引數就不應該使用指標。常見的例項包括傳遞一個指標給一個 string (string),或是一個指標給一個 interface 值(*io.Reader)。在兩個情況中,它自己的值都是不變的,可以被直接傳遞。這個建議不適用於大的 structs,或是會增長的小的 structs。

Receiver Names

一個方法的 receiver 的名字應該是它身份的一個反射;通常一兩個字母是足夠滿足它型別的縮寫(比如,”c” or “cl” 作為 “Client” 的縮寫)。不要使用通用名稱,比如 “me”, “this” or “self”,面嚮物件語言的典型標識是更注重方法而不是函式。該名字不必作為方法引數的描述,它的角色是非常明顯的,並且不是作為文件目的。它可以是非常短的,因為它出現在每一個方法的幾乎每一行上;熟悉承認整潔。需要保持一致,如果你在一個方法中呼叫 “c” receiver,不能在另外一個方法中呼叫 “cl”。

Receiver Type

在一個方法上選擇是使用一個值 receiver 還是指標 receiver 是非常困難的,特別是對於 Go 新手。如果不能肯定,那就使用指標,但是有時候一個值 receiver 更有意義。通常是因為效率的原因,比如小的不變的 structs 或是基礎型別的值。
根據經驗,一般來說:

  • 如果 receiver 是一個 map,func 或 chan,不要使用指標
  • 如果 receiver 是一個 slice 和 方法不再 reslice 或是 重分配的 slice,不要使用指標
  • 如果方法需要可變的 receiver,receiver 必須使用指標
  • 如果 receiver 是一個包含 sync.Mutex 或是類似的 synchronizing 屬性的 struct,receiver 必須是指標以避免複製
  • 如果 receiver 是一個大的 struct 或是 array,一個指標 receiver 會更有效率。多大是大?假設它是要傳遞它所有的元素作為方法的引數。如果感到太大,它同樣對於 receiver 也是大的。
  • Can function or methods, either concurrently or when called from this method, be mutating the receiver? 當方法被呼叫是,一個值型別的副本被建立,因此外部更新,不會影響 receiver。如果在原始的 receiver 中改變必須是可見的,那 receiver 必須是指標。
  • 如果 receiver 是一個 struct, array 或 slice 和 它任何一個元素是指標,那它即是可變的。更喜歡指標 receiver,因為對於讀者來說,它的意圖更加清晰
  • 如果 receiver 是小的 array 或 struct,那自然是值型別(比如, time.Time 型別),沒有可變的屬性和沒有指標,或者僅僅是一個簡單的基礎型別,比如 int 或 string,一個值 receiver 會更有意義。一個值 receiver 可以減少生成的記憶體垃圾數量;如果一個值被傳遞給一個值方法,一個棧拷貝將被使用,而不是在 heap 上分配記憶體(編譯器嘗試聰明的避免分配記憶體,但它可能不會一直成功)。在沒有優化前,沒有因為這個原因選擇一個值 receiver。
  • 最後,當不確定的時候,請使用指標 receiver

Useful Test Failures

測試應該在失敗的時候伴隨著輸出有幫助的資訊告訴你失敗原因是什麼,輸入是什麼,實際發生了什麼,期望值是什麼。它很可能成為寫一堆 assertFoo 的幫手。但是確保你的幫手生成了有用的錯誤資訊。假設一個不是你的人,或者不是你團隊的人在 debugging 你的錯誤測試。一個典型的 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, “期望 x 獲得 0”等等,Go 不這樣做。

如果看起來有很多型別,你可能想寫一個 table-driven 測試:http://code.google.com/p/go-wiki/wiki/TableDrivenTests

另外一個常用的技術是消除錯誤測試的歧義,當使用一個有不同輸入的來使用一個不同的 TestFoo 函式來包裝每個呼叫測試幫手的時候,因此這個失敗測試使用的名字為:

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

在任何情況下,你的責任是不管以後誰除錯你的程式碼,當失敗的時候,都應該輸出有用的資訊。

變數命名

在 Go 中的變數名應該短而不是長。對於空間有限的區域性變數尤其如此。更喜歡 c 表示行數,喜歡 i 表示 slice 的索引。

基本的規則是:進一步宣告,一個名字被使用,這個名字必須描述更多的資訊。對於一個方法 receiver,一個或兩個字母是合適的。普通的變數比如 loop indices 和 readers 可以是單個字母(i, r)。更不尋常的事情和全域性變數需要更具描述性的名稱。

相關文章