Go 部落格
包名
簡介
Go 程式碼以包為單位進行組織。在一個包內部,程式碼可以引用包內定義的任何識別符號(名稱),而包的客戶端只能引用包匯出的型別、函式、常量和變數。這類引用總是包含包名作為字首:foo.Bar
指的是匯入的名為 foo
的包中匯出的名稱 Bar
。
好的包名能讓程式碼更好。包名提供了其內容的上下文,使客戶端更容易理解包的作用以及如何使用它。該名稱還有助於包維護者在包演進過程中確定哪些內容屬於或不屬於該包。命名良好的包使得查詢所需的程式碼更加容易。
高效 Go 程式設計提供了關於包、型別、函式和變數命名的指導方針。本文在此討論的基礎上進行擴充套件,並考察了標準庫中的命名。本文還討論了不好的包名以及如何改進它們。
包名
好的包名簡潔明瞭。它們使用小寫字母,不包含 under_scores
或 mixedCaps
。它們通常是簡單的名詞,例如
time
(提供時間測量和顯示功能)list
(實現雙向連結串列)http
(提供 HTTP 客戶端和伺服器實現)
另一種語言中典型的命名風格在 Go 程式中可能不符合習慣。以下是兩個在其他語言中可能風格良好但在 Go 中不太合適的名稱示例
computeServiceClient
priority_queue
一個 Go 包可以匯出多種型別和函式。例如,一個 compute
包可以匯出一個帶有使用該服務方法的 Client
型別,以及用於將計算任務分配到多個客戶端的函式。
謹慎地縮寫。當縮寫為程式設計師所熟悉時,可以縮寫包名。廣泛使用的包通常採用壓縮名稱
strconv
(字串轉換)syscall
(系統呼叫)fmt
(格式化輸入/輸出)
另一方面,如果縮寫包名使其變得模糊不清,就不要這樣做。
不要佔用使用者常用的好名稱。避免給包命名為客戶端程式碼中常用的名稱。例如,緩衝 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
在包 pkg
中名為 New
的函式返回型別為 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.Reader
、bufio.Reader
和 csv.Reader
。每個包名與 Reader
結合,都形成了良好的型別名稱。
如果您無法為包內容想出一個有意義字首的包名,那麼包的抽象邊界可能存在問題。編寫使用您的包的程式碼,就像客戶端會做的那樣,如果結果看起來不理想,就重構您的包。這種方法將產生客戶端更容易理解、包開發者更容易維護的包。
包路徑
Go 包同時擁有名稱和路徑。包名在其原始檔的 package 宣告中指定;客戶端程式碼將其用作包匯出名稱的字首。客戶端程式碼在匯入包時使用包路徑。按照慣例,包路徑的最後一個元素就是包名。
import (
"context" // package context
"fmt" // package fmt
"golang.org/x/time/rate" // package rate
"os/exec" // package exec
)
構建工具將包路徑對映到目錄。go 工具使用 GOPATH 環境變數在目錄 $GOPATH/src/github.com/user/hello
中查詢路徑 "github.com/user/hello"
的原始檔。(當然,這種情況應該是熟悉的,但明確包的術語和結構很重要。)
目錄。標準庫使用像 crypto
、container
、encoding
和 image
這樣的目錄來分組相關的協議和演算法包。這些目錄中的包之間沒有實際關係;目錄只是提供了一種檔案組織方式。任何包都可以匯入任何其他包,只要匯入不會建立迴圈。
正如不同包中的型別可以擁有相同的名稱而沒有歧義一樣,不同目錄中的包也可以擁有相同的名稱。例如,runtime/pprof 提供 pprof 分析工具所需的格式的分析資料,而 net/http/pprof 提供 HTTP 端點以這種格式呈現分析資料。客戶端程式碼使用包路徑匯入包,因此不會引起混淆。如果原始檔需要匯入兩個 pprof
包,它可以在本地重新命名其中一個或兩個。重新命名匯入的包時,本地名稱應遵循包名的相同準則(小寫,不帶 under_scores
或 mixedCaps
)。
不好的包名
不好的包名使得程式碼難以導航和維護。以下是一些識別和修復不好名稱的指導方針。
避免無意義的包名。名為 util
、common
或 misc
的包無法向客戶端說明包中包含什麼。這使得客戶端更難使用該包,也使得維護者更難保持包的專注性。隨著時間的推移,它們會積累依賴項,這可能會顯著且不必要地減慢編譯速度,尤其是在大型程式中。而且由於這些包名是通用的,它們更容易與客戶端程式碼匯入的其他包發生衝突,迫使客戶端不得不創造名稱來區分它們。
拆分通用包。要改進這類包,請尋找具有共同名稱元素的型別和函式,並將它們提取到它們自己的包中。例如,如果您有
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。許多好心的程式設計師將程式公開的所有介面放在名為 api
、types
或 interfaces
的單個包中,認為這樣可以更容易找到程式碼庫的入口點。這是一個錯誤。這類包存在與 util
或 common
包相同的問題,它們會無限增長,無法為使用者提供指導,積累依賴項,並與其他匯入發生衝突。將它們拆分開,也許可以使用目錄來區分公共包和實現。
避免不必要的包名衝突。雖然不同目錄中的包可能同名,但經常一起使用的包應該有不同的名稱。這可以減少混淆,並減少客戶端程式碼中本地重新命名的需要。出於同樣的原因,避免使用與 io
或 http
等流行標準包相同的名稱。
結論
包名是 Go 程式中良好命名的核心。花時間選擇好的包名並妥善組織您的程式碼。這有助於客戶端理解和使用您的包,並有助於維護者使其優雅地成長。
進一步閱讀
下一篇文章:Go 中的可測試示例
上一篇文章:錯誤是值
部落格索引