Go 部落格

包名

Sameer Ajmani
2015 年 2 月 4 日

引言

Go 程式碼組織在包中。在包內部,程式碼可以引用包中定義的任何識別符號(名稱),而包的客戶端只能引用包的匯出型別、函式、常量和變數。此類引用始終包含包名作為字首:foo.Bar 引用了名為 foo 的已匯入包中的匯出名稱 Bar

好的包名能讓程式碼更好。包的名稱為其內容提供了上下文,使客戶端更容易理解包的作用以及如何使用它。名稱也有助於包維護者確定包在演進過程中哪些內容屬於它,哪些不屬於它。命名良好的包使查詢所需程式碼更加容易。

Effective Go 提供了有關命名包、型別、函式和變數的指南。本文擴充套件了該討論,並調查了標準庫中發現的名稱。它還討論了不良的包名稱以及如何修復它們。

包名

好的包名簡短而清晰。它們是小寫的,沒有 下劃線駝峰式混合大小寫。它們通常是簡單的名詞,例如

  • time(提供測量和顯示時間的功能)
  • list(實現雙向連結串列)
  • http(提供 HTTP 客戶端和伺服器實現)

其他語言中典型的名稱風格在 Go 程式中可能不符合慣例。以下是兩個在其他語言中可能是良好風格但在 Go 中不適合的名稱示例

  • computeServiceClient
  • priority_queue

一個 Go 包可以匯出幾種型別和函式。例如,一個 compute 包可以匯出一個 Client 型別,其中包含用於使用該服務的相關方法以及用於在多個客戶端之間劃分計算任務的函式。

明智地縮寫。 當程式設計師熟悉縮寫時,包名可以被縮寫。廣泛使用的包通常有縮短的名稱

  • strconv(字串轉換)
  • syscall(系統呼叫)
  • fmt(格式化 I/O)

另一方面,如果縮寫包名會使其含糊不清或不明確,則不要這樣做。

不要竊取使用者的好名字。 避免給包命名一個在客戶端程式碼中常用的名稱。例如,緩衝 I/O 包名為 bufio,而不是 buf,因為 buf 是一個好的緩衝區變數名。

命名包內容

包名及其內容的名稱是耦合的,因為客戶端程式碼將它們一起使用。在設計包時,請從客戶端的角度出發。

避免重複。 由於客戶端程式碼在引用包內容時使用包名作為字首,因此這些內容的名字不必重複包名。http 包提供的 HTTP 伺服器稱為 Server,而不是 HTTPServer。客戶端程式碼將此型別引用為 http.Server,因此沒有歧義。

簡化函式名。 當包 pkg 中的函式返回型別為 pkg.Pkg(或 *pkg.Pkg)的值時,函式名通常可以省略型別名而不會產生混淆

start := time.Now()                                  // start is a time.Time
t, err := time.Parse(time.Kitchen, "6:06PM")         // t is a time.Time
ctx = context.WithTimeout(ctx, 10*time.Millisecond)  // ctx is a context.Context
ip, ok := userip.FromContext(ctx)                    // ip is a net.IP

名為 New 的函式在 pkg 包中返回型別為 pkg.Pkg 的值。這是客戶端程式碼使用該型別的標準入口點

 q := list.New()  // q is a *list.List

當函式返回型別為 pkg.T 的值,其中 T 不是 Pkg 時,函式名可以包含 T 以便客戶端程式碼更容易理解。一種常見情況是具有多個類似 New 的函式的包

d, err := time.ParseDuration("10s")  // d is a time.Duration
elapsed := time.Since(start)         // elapsed is a time.Duration
ticker := time.NewTicker(d)          // ticker is a *time.Ticker
timer := time.NewTimer(d)            // timer is a *time.Timer

不同包中的型別可以具有相同的名稱,因為從客戶端的角度來看,這些名稱由包名區分。例如,標準庫包含幾個名為 Reader 的型別,包括 jpeg.Readerbufio.Readercsv.Reader。每個包名與 Reader 結合,產生一個好的型別名。

如果您無法想出一個有意義地作為包內容字首的包名,那麼包的抽象邊界可能不正確。編寫像客戶端一樣使用您的包的程式碼,如果結果不理想,請重構您的包。這種方法將產生更易於客戶端理解且對包開發者更易於維護的包。

包路徑

Go 包既有名稱也有路徑。包名在其原始檔中的包宣告中指定;客戶端程式碼將其用作包匯出名稱的字首。客戶端程式碼在匯入包時使用包路徑。按照慣例,包路徑的最後一個元素是包名

import (
    "context"                // package context
    "fmt"                    // package fmt
    "golang.org/x/time/rate" // package rate
    "os/exec"                // package exec
)

構建工具將包路徑對映到目錄。go 工具使用 GOPATH 環境變數來查詢路徑 "github.com/user/hello" 的原始檔,目錄是 $GOPATH/src/github.com/user/hello。(這種情況應該很熟悉,當然,但清楚包的術語和結構很重要。)

目錄。 標準庫使用 cryptocontainerencodingimage 等目錄來分組相關協議和演算法的包。這些目錄中的包之間沒有實際關係;目錄只是提供了一種組織檔案的方式。任何包都可以匯入任何其他包,前提是匯入不產生迴圈。

就像不同包中的型別可以無歧義地擁有相同的名稱一樣,不同目錄中的包也可以擁有相同的名稱。例如,runtime/pprofpprof 分析工具期望的格式提供分析資料,而 net/http/pprof 提供 HTTP 端點以該格式呈現分析資料。客戶端程式碼使用包路徑匯入包,因此沒有混淆。如果原始檔需要匯入兩個 pprof 包,它可以本地重新命名其中一個或兩個。重新命名已匯入的包時,本地名稱應遵循與包名相同的指南(小寫,無 下劃線駝峰式混合大小寫)。

不良的包名

不良的包名會使程式碼更難導航和維護。以下是一些識別和修復不良名稱的指南。

避免無意義的包名。 命名為 utilcommonmisc 的包不會給客戶端帶來任何關於包內容的含義。這使得客戶端更難使用該包,也使得維護者更難保持包的專注。隨著時間的推移,它們會累積依賴項,這會顯著且不必要地減慢編譯速度,尤其是在大型程式中。而且由於此類包名是通用的,因此它們更有可能與其他客戶端程式碼匯入的包發生衝突,迫使客戶端發明名稱來區分它們。

拆分通用包。 要修復此類包,請查詢具有通用名稱元素的型別和函式,並將它們提取到自己的包中。例如,如果您有

package util
func NewStringSet(...string) map[string]bool {...}
func SortStringSet(map[string]bool) []string {...}

那麼客戶端程式碼看起來像

set := util.NewStringSet("c", "a", "b")
fmt.Println(util.SortStringSet(set))

將這些函式從 util 提取到一個新包中,選擇一個適合其內容的名稱

package stringset
func New(...string) map[string]bool {...}
func Sort(map[string]bool) []string {...}

然後客戶端程式碼變為

set := stringset.New("c", "a", "b")
fmt.Println(stringset.Sort(set))

進行此更改後,更容易看出如何改進新包

package stringset
type Set map[string]bool
func New(...string) Set {...}
func (s Set) Sort() []string {...}

這會產生更簡單的客戶端程式碼

set := stringset.New("c", "a", "b")
fmt.Println(set.Sort())

包的名稱是其設計中的關鍵部分。努力消除專案中的無意義包名。

不要為所有 API 使用單個包。 許多好心程式設計師將程式暴露的所有介面放在一個名為 apitypesinterfaces 的包中,認為這樣可以更容易地找到程式碼庫的入口點。這是一個錯誤。此類包與命名為 utilcommon 的包存在相同的問題,無限制地增長,不提供使用者指導,累積依賴項,並與其他匯入發生衝突。將它們拆分,也許使用目錄將公共包與實現分開。

避免不必要的包名衝突。 雖然不同目錄中的包可能名稱相同,但經常一起使用的包應具有不同的名稱。這可以減少混淆和客戶端程式碼中本地重新命名的需要。出於同樣的原因,避免使用與 iohttp 等流行標準包相同的名稱。

結論

包名是 Go 程式中良好命名的核心。花時間選擇好的包名並妥善組織程式碼。這有助於客戶端理解和使用您的包,並幫助維護者優雅地擴充套件它們。

延伸閱讀

下一篇文章:Go 中的可測試示例
上一篇文章:錯誤是值
部落格索引