Go 語言中包的風格指南

語言: CN / TW / HK

爭做團隊核心程序員,關注「 幽鬼

轉自「Go語言中文網」

Go 語言也有自己的命名與代碼組織規則。漂亮的代碼,佈局清晰、易讀易懂,就像是設計嚴謹的 API 一樣。拿到代碼,用户首先看到和接觸的就是佈局、命名還有包的結構。

這篇文章不是為了給大家設立硬性的規定,而是用實踐引導大家形成自己的規則。因為具體問題要具體分析,通過自己的判斷才能挑選出最恰當的規則。

所有的 Go 代碼都是以包的形式組織起來的。Go 中的包就是目錄或文件夾裏面包括一個或者多個以 .go 結尾的文件。用目錄或文件夾這樣的方式來管理代碼,與電腦管理目錄或文件夾是一樣一樣的。

所有的 Go 代碼都是放在包裏的,且只能是通過包來進行訪問。理解並且建立良好的包的習慣,可以幫助寫出高效的代碼。

包的組織

我們先聊聊如何組織 Go 代碼,解釋一下定位 Go 包的規範。

使用多個文件

一個包就是一個或者多個文件的目錄。先把代碼切分成符合邏輯且易讀的多個文件。

例如,根據文件處理 HTTP 的內容不一樣,一個 HTTP 包可以被切分成多個文件。在下面這個例子中,一個 HTTP 包被切成下列文件:頭部類型定義和代碼,cookie 類型定義加代碼,還有實際 HTTP 功能的實現和包説明文檔。

- doc.go       // 包説明文檔
- headers.go   // HTTP 頭部類型定義和代碼
- cookies.go   // HTTP cookie 類型定義和代碼
- http.go      // HTTP 客户端實現,請求和返回類型,等等

聚合類型定義

首要規則是,把類型定義儘量都聚合到他們被引用的地方。這讓代碼的維護者(不僅僅侷限於代碼的原作者)更易於找到類型的定義。比如,頭結構體類型最好就是放在 headers.go 文件當中。

$ cat headers.go
package http

// Header 表示一個 Http 頭部結構體定義
type Header struct {...}

雖然 Go 語言本身並沒有嚴格地要求你必須在文件哪個部分定義類型,但是把核心類型的定義都放在文件的最上面是沒錯的。

根據功能進行安排

在其他語言中,通常都是把類型定義聚合到一個包裏,叫做模型或者類別。在 Go 語言中,則是通過代碼的功能職責來進行安排的。

package models // 千萬別叫這個名字

// User 代表系統中的一個用户
type User struct {...}

不要創建一個命名為 models 的包,然後在裏面定義所有的實體類型。在這個例子中,User 類型應該定義在服務層的包中。

package mngtservice

// User 代表系統中的一個客户
type User struct {...}

func UsersByQuery(ctx context.Context, q *Query) ([]*User, *Iterator, error)

func UserIDByEmail(ctx context.Context, email string) (int64, error)

優化 godoc

越早使用 godoc 越好,尤其是在初期設計包的 API 的時候。使用 godoc,你可以清楚知道自己的構思用文檔表達出來是個什麼。有時候,可視化也對設計有影響。因為 godoc 需要放在一個獨立的包裏,所以可以慢慢地來進行優化,讓文檔越來越容易理解。執行命令 godoc -http=<hostport> 來啟動本地的 godoc 文檔服務。

舉幾個例子説明一下

有些情況下,你可能沒辦法把所有相關的類型定義都寫在一個包裏。因為這樣做可能會很繁瑣。或者你只是想發佈一個實現了單個包中通用接口的具體功能,或者這些類型是由第三方包提供的。下面給出幾個例子來説明和理解這些情況。

$ godoc cloud.google.com/go/datastore
func NewClient(ctx context.Context, projectID string, opts ...option.ClientOption) (*Client, error)
...

NewClient 這個方法裏有一個結構參數 option.ClientOptions,但是這個結構的定義既不在 datastore 包裏,也不在提供所有 option 類型的 option 包裏。

$ godoc google.golang.org/extraoption
func WithCustomValue(v string) option.ClientOption
...

如果你設計的 API 需要引入很多非標準庫的包,那麼,添加 Go 示例 [1] 通常會很有用,以便為用户提供一些工作代碼。

樣例可以提高藏地較深的包的暴光率。例如,datastore.NewCLient 這樣的結構需要引用額外的 option 包。提供樣例,就可以讓用户知道,還有一個 option 包。

不要在 main 文件中導出

標識符可以被 導出 [2] ,以允許從外部包來使用它。

main 包是不能被導入的,所以從 main 包中導出標記符是沒有必要的。如果你要把包編譯成二進制文件,就不要從 main 包中導出標記符。

這條規則也有例外,那就是 main 包被編譯成了 .so 文件、.a 文件或者 Go 插件。在這種情況下,Go 代碼被其他語言通過 cgo 的導出功能 [3] 使用,那標識符的導出就是必要的了。

包的命名

包的名字與導入路徑,都是很重要的標識,它們會告訴你這個包裏有哪些內容。按規則給包命名不僅可以提高你的代碼的質量,也間接地提高這個包的使用者的代碼水平。

只用小寫

包的名字應只用小寫。不要用下劃線式,也不要用駝峯式。Go 官方博文 關於包命名的綜合指南 [4] 中有多個不同情形的樣例。

簡短而有意義

包的名字需要簡短,但應該唯一且有意義。用户從包的名字中就能直接理解這個包的作用。

避免泛泛的包名,例如 "common", "util"。

import "pkgs.org/common" // 可千萬別這樣寫

避免重名,萬一用户要同時引入並使用這兩個同名包。

如果命名上確實有困難,可能是因為設計的代碼結構與整體構架本身就有問題。

精簡引入路徑

避免暴露自定義的倉庫結構(repository structure)給包的用户。謹遵 GOPATH 的規定。避免在引入路徑中出現包含 src/, pkg/ 命名的路徑。

github.com/user/repo/src/httputil   // 可千萬別這麼做,不要使用 SRC !!

github.com/user/repo/gosrc/httputil // 可千萬別這麼做,不要使用 GOSRC !!

使用單數

在 Go 語言中,包的名字不要使用複數。從其他語言轉過來的程序員會覺得很彆扭,因為在先前使用的語言中,已經形成了使用複數的習慣。給包命名的時候,用 httputil,不用 httputils!

package httputils  // 用單數,不用複數

別名也應該遵守規則

如果你在引入多個相同名字的包,你可以在本地修改這些包的名字。別名也需要遵守本文提到的規則。並沒有規則指明需要修改哪一個包的名字。如果你在修改標準庫包的名字,最好加一個前綴來做區別,畢竟是 “Go 標準庫” 中的包,比如,可以修改為 gourlgoioutil

import (
    gourl "net/url"

    "myother.com/url"
)

強制使用虛擬 URL

go get 支持通過另外一種 URL 來獲取包,這個 URL 與包倉庫的 URL 的名字不一樣。這個不一樣的 URL 叫做虛擬 URL,需要準備一個頁面,裏面包含可被 Go 工具識別的詳細的元標籤。你可以使用虛擬 URL 通過自定義域名和路徑來提供包的服務。

例如,

$ Go get cloud.google.com/go/datastore

在後台去查看來自 https://code.googlesource.com/gocloud 的源碼,把它加到你的工作區當中去,這個工作區是定義在 $GOPATH/src/cloud.google.com/go/datastore 下面的。

假定 code.googlesource.com/gocloud 已經在包裏了,那能不能通過這個 URL 來使用這個包呢?答案是 NO,因為開啟了強制使用虛擬 URL。

實際使用中,在包裏添加了一個引入聲明。Go 工具就不允許從任何其他路徑來引入這個包,並且會給用户一個友好的錯誤提示。如果你沒有開啟強制使用虛擬 URL,那麼就會有出現兩個一樣的包,並且因為不同的命名空間,它們不能放在一起使用。

package datastore // import "cloud.google.com/go/datastore"

包説明文檔

記得要給包寫説明文檔。包説明文檔最可以闡明包的功能。對於非 main 包來講,godoc 都是以 "Package {包名}" 開頭,並且附上一個描述説明。對於 main 包來講,文檔就是用來説明程序的功能的。

// Package ioutil 實現了一些輸入或輸出效用功能
package ioutil

// gops 命令會列出所有在系統中跑的進程
package main

// helloworld 樣例來展示如何使用 x 功能
package main

使用 doc.go 文件

有時候,包裏的文檔説明內容會很多,尤其是包括詳細的用法説明與指導。將包的 godoc 移到 doc.go 這個文件中去。(參考樣例 doc.go [5] .)

via: https://rakyll.org/style-packages/

作者: rakyll [6] 譯者: chenbrooks [7] 校對: polaris1119 [8]

本文由 GCTT [9] 原創編譯, Go 中文網 [10] 榮譽推出,首發於 https://studygolang.com/articles/11823。

參考資料

[1]

Go 示例: https://blog.golang.org/examples

[2]

導出: https://golang.org/ref/spec#Exported_identifiers

[3]

cgo 的導出功能: https://golang.org/cmd/cgo/#hdr-C_references_to_Go

[4]

關於包命名的綜合指南: https://blog.golang.org/package-names

[5]

doc.go: https://github.com/GoogleCloudPlatform/google-cloud-go/blob/master/datastore/doc.go

[6]

rakyll: https://rakyll.org/about/

[7]

chenbrooks: https://github.com/chenbrooks

[8]

polaris1119: https://github.com/polaris1119

[9]

GCTT: https://github.com/studygolang/GCTT

[10]

Go 中文網: https://studygolang.com/

往期推薦

歡迎關注「 幽鬼 」,像她一樣做團隊的核心。