Go 語言的包管理機制(import,package)
前言
任何包管理系統的目的都是通過對關聯的特性進行分類,組織成便於理解和修改的單元,使其與程序的其他包保持獨立,從而有助於設計和維護大型的程序。模塊化允許包在不同的項目中共享、複用,在組織中發佈,或者在全世界範圍內使用。
命名空間
每個包定義了一個不同的命名空間作爲它的標識符。
每個名字關聯一個具體的包,它讓我們在爲類型、函數等選取短小而且清晰的名字的同時,不與程序的其他部分衝突。
封裝
包通過控制名字 " 是否導出 " 是其對包外可見來提供封裝能力。
限制包成員的可見性,從而隱藏 API 後面的輔助函數和類型,允許包的維護者修改包的實現而不影響包外部的代碼。
限制變量的可見性也可以隱藏變量,這樣使用者僅可以通過導出函數來對其訪問和更新,他們可以保留自己的不變量以及在併發程序中實現互斥的訪問。
Go 編譯
當我們修改一個文件時,我們必須重新編譯文件所在的包和所有潛在依賴它的包。
Go 程序的編譯比其他語言要快,即便從零開始編譯也如此。這裏有三個主要的原因:
**第一:**所有的導入都必須在每一個源文件的開頭進行顯式列出,這樣編譯器在確定依賴性的時候就不需要讀取和處理整個文件。
**第二:**包的依賴性形成有向無環圖,因爲沒有環,所以包還可以獨立甚至並行比編譯。
**第三:**Go 包編譯輸出的目標文件不僅記錄它自己的導出信息,還記錄它所依賴包的導出信息。當編譯一個包時,編譯器必須從每一個導入中讀取一個目標文件,但是不會超出這些文件。
導入路徑
每一個包都通過一個唯一的字符串進行標識,它稱爲 " 導入路徑 ",它們在 import 聲明中。
例如:
import (
"fmt"
"math/rand"
"encoding/json"
"golang.org/x/net/html"
"gitHub.com/go-sql-driver/mysql"
)
標準庫的包與非標準庫的包導入規範
Go 語言的規範沒有定義字符串的含義或如何確定一個包的導入路徑,它通過工具來解決這些問題,在後面的文章中我們會討論 go 工具。
對於準備共享或公開的包,導入路徑需要全局唯一。
爲了避免衝突,建議,除了標準庫中的包之外,其他包的導入路徑應該以互聯網域名(組織機構擁有的域名或用於存放包的域名)作爲路徑開始,這樣也方便查找包。(例如上面導入 Go 團隊維護的一個 HTML 解析器和一個流行的第三方 MySQL 數據庫驅動程序)。
想要導入非標準庫的包,導入路徑需要從 GOPATH/src / 開始導入,否則無法尋找。
導入自定義包演示案例
我們建立好目錄,然後將其設置到 GOPATH 環境變量中。
此時我們在 GOPATH/src//code.dongshao.com/studygo/testpack / 目錄下建立一個 calc.go 文件,然後將它的包命名爲 calc。
備註:上面我們爲了演示,把包名和目錄名定義的不一致,當時在實際編程中,建議把包名和目錄名定義一致。
現在我們想在另一個目錄下的. go 文件中調用上面的 calc 包,那麼需要從 GOPATH 路徑的 src 目錄開始導入,如下所示:
包的聲明(package)
在每一個 Go 源文件的開頭都需要進行包聲明。聲明該文件歸屬的包,主要的目的是當該包被其他包引入的時候作爲其默認的標識符(稱爲包名)。
注意事項:
-
一個文件夾下面只能有一個包,一個包的文件不能在多個文件夾下。
-
一個. go 文件 package 定義的包名可以不和自己當前所在文件夾的名字一樣(編程時建議改成一致的)。
-
包名不能包含 - 符號。
-
包名爲 main 的包爲應用程序的入口包,編譯時不包含 main 包的源代碼是不會得到可執行文件。
例如,math/rand 包中的每一個文件的開頭都是 package rand。這樣當你導入這個包時,可以訪問它的成員,比如 rand.Int、rand.Float64 等。
package main
import (
"fmt"
"math/rand"
)
func main() {
fmt.Println(rand.Int())
}
包名衝突
通常,包名是導入路徑的 "最後一段",於是,即使導入路徑不同的兩個包,二者也可以擁有同樣的名字。
例如,一個程序中導入的兩個包名分別是 math/rand 和 crypto/rand,而包的名字都是 rand,這樣就產生衝突了。
關於衝突的解決辦法,可以參閱文章下面的 "重命名導入"。
包名的幾個注意事項
上面介紹了,包名就是導入路徑的 "最後一段",關於 "最後一段" 還有三個例外。
**第一個例外:**不管包的導入路徑是什麼,如果該包定義一條命令(可執行的 Go 程序),那麼它總是使用名稱 main。就是告訴 go build 的信號,它必須調用連接器生成可執行文件。
**第二個例外:**目錄中可能有一些文件名字以_test.go 結尾,包名中會出現以_test 結尾。這樣一個目錄中有兩個包:一個普通的,另外一個是外部測試包。_test 後綴告訴 go test 兩個包都需要構建,並且指明文件屬於哪個包。外部測試包用來避免測試所依賴的導入圖中的循環依賴。(在 "測試" 文章的 "外部測試包" 相關專題中會詳細講述)。
**第三個例外:**有一些依賴管理工具會在包導入路徑的尾部追加版本後綴,如 "gopkg.in/yaml.v2"。包名不包含後綴,因此這個情況下包名爲 yaml。
導入聲明(import)
一個 Go 源文件可以在 package 後面調用 import 導入包。
import 的兩種語法格式:
-
每個包單獨使用一個 import 語句。
-
多個包使用同一個 import 語句。
例如,下面兩種形式都是等價的。
import "fmt"
import "os"
import (
"fmt"
"os"
)
分組規範
導入的包可以通過空行進行分組;這類分組通常表示不同領域和方面的包。
導入順序不重要,但建議每一組都按照字母進行排序(gofmt 和 goimports 工具都會自動進行分組並排序)。
例如:
import (
"fmt"
"html/template"
"os"
"golang.org/x/net/html"
"golang.org/x/net/ipv4"
)
重命名導入
如果需要把兩個名字一樣的包(例如 math/rand 和 crypto/rand)導入到第三個包中,導入聲明就必須至少爲其中的一個置頂一個替代名字來避免衝突。這叫做重命名導入。
例如:
import "crypto/rand"
import mrand "math/rand" // 重命名爲rand
import (
"crypto/rand"
mrand "math/rand" // 重命名爲rand
)
重命名導入在沒有衝突時也是非常有用的。如果有時用到自動生成的代碼,導入的包名字非常冗長,使用一個替代名字可能更方便。
重命名之後的名字建議一直用下去,以避免產生混淆。
使用一個替代名字有助於規避常見的局部變量衝突。例如,如果一個文件可以包含許多以 path 命名的變量,我們就可以使用 pathpkg 這個名字導入一個標準的 "path" 包。
包循環依賴
Go 語言禁止循環導入包。
例如,A 包導入 B 包,B 包又導入 A 包。
每個導入聲明從當前包嚮導入的包建立一個依賴。如果這些依賴形成一個循環,go build 工具會報錯。
空導入(_)
前面我們介紹過了,如果導入的包的名字沒有在文件中引用,就會產生一個編譯錯誤。
但是有時候我們必須導入一個包,這僅僅是爲了利用其副作用:對包級別的變量執行初始化表達式求值,並執行它的 init 函數。
空導入(匿名導入)
爲了防止 "未使用的導入" 錯誤,我們必須使用一個重命名導入,它使用一個替代的名字_,這表示導入的內容爲空白標識符。這稱爲 "空白導入"。
通常情況下,空白標識符不可能被引用。
例如:
import _ "image/png"
多數情況下,它用來實現一個編譯時的機制,使用空白引用導入額外的包,來開啓主程序中可選的特性。
例如,在包 A 中執行導入包 B 的 import 語句時,會自動觸發包 B 的 init() 函數。我們在 A 包空導入 B 包,那麼就可以執行 B 包的 init() 函數(例如在函數中初始化數據庫等),對於 B 包的其餘成員和方法我們都不使用。
備註:在自己的文件中(例如 main 包)也可以調用 init() 函數。
演示案例
標準庫的 image 包導出了 Decode 函數,它從 io.Reader 讀取數據,並且識別使用哪一種圖像格式來編碼數據,調用適當的解碼器,返回 image.Image 對象作爲結果。使用 image.Decode 可以構建一個簡單的圖像轉換器,讀取某一種格式的圖像,然後輸出爲另外一個格式:
package main
import (
"fmt"
"image"
"image/jpeg"
_ "image/png" // register PNG decoder
"io"
"os"
)
func main() {
if err := toJPEG(os.Stdin, os.Stdout); err != nil {
fmt.Fprintf(os.Stderr, "jpeg: %v\n", err)
os.Exit(1)
}
}
func toJPEG(in io.Reader, out io.Writer) error {
img, kind, err := image.Decode(in)
if err != nil {
return err
}
fmt.Fprintln(os.Stderr, "Input format =", kind)
return jpeg.Encode(out, img, &jpeg.Options{Quality: 95})
}
此處我們藉助之前文章中使用到的一個例子:
在上面的文章中有一個浮點數繪圖的例子,我們使用這個程序(mandelbrot)的輸出作爲這個轉換程序的輸入,它檢測 PNG 個數的輸入,然後輸出 JPEG 格式的圖。
$ ./mandelbrot | ./jpeg > mandelbrot.jpg
Input format = png
如果我們把空包導入註釋,如下所示:
import (
"fmt"
"image"
"image/jpeg"
// _ "image/png" // register PNG decoder
"io"
"os"
)
如果沒有上面哪一行,程序可以正常編譯和鏈接,但是不能識別和解碼 PNG 格式的輸入:
$ ./mandelbrot | ./jpeg > mandelbrot.jpg
jpeg: image: unknow format
這裏解釋它是如何工作的。標準庫提供 GIF 、PNG 、JPEG 等格式的解碼庫,用戶自己可以提供其他格式的,但是爲了使可執行程序簡短,除非明確需要,否則解碼器不會被包含進應用程序。image.Decode 函數查閱一個關千支持格式的表格。每一個表項由 4 個部分組成:格式的名字;某種格式中所使用的相同的前綴字符串,用來識別編碼格式;一個用來解碼被編碼圖像的函數 Decode ; 以及另一個函數 DecodeConfig, 它僅僅解碼圖像的元數據,比如尺寸和色域。對於每一種格式,通常通過在其支持的包的初始化函數中來調用 image.RegisterFormat 來向表格添加項,例如 image/png 中的實現如下:
package png // image/png
func Decode(r io.Reader) (image.Image, error)
func DecodeConfig(r io.Reader) (image.Config, error)
func init() {
canst pngHeader = "\x89PNG\r\n\xla\n"
image.RegisterFormat("png", pngHeader, Decode, DecodeConfig)
}
這個效果就是,一個應用只需要空白導入格式化所需的包,就可以讓 image.Decode 函數具備應對格式的解碼能力。
database/sql 包使用類似的機制讓用戶按需加入想要的數據庫驅動程序。例如:
import (
"database/sq!"
_ "github.com/lib/pq" // 添加Postgres 支持
_ "github.com/go-sql-driver/mysql" // 添加MySQL 支持
)
db, err= sql.Open("postgres", dbname) // OK
db, err= sql.Open("mysql", dbname) // OK
db, err= sql.Open("sqlite3", dbname) // 返回錯誤消息: unknown driver "sqlite3"
包命名
下面介紹一些包及其命名規範,不是強制的,是一些建議。
使用簡短的包名
當創建一個包,一般要用簡短的包名,但也不能太短導致難以理解。標準庫中最常用的包有 bufio、bytes、flag、fmt、http、io、json、os、sort、sync 和 time 等包。
保證可讀性和無歧義
例如,不要把一個輔助工具包命名爲 util,使用 imageutil 或 ioutil 等名稱更具體和清晰。
避免選擇經常用於相關的局部變量的包名,或者迫使使用者使用重命名導入,例如使用以 path 命名的包。
包名通常使用統一的形式
標準庫的 bytes、errors 和 strings 使用了複數形式來避免覆蓋響應的預聲明類型,使用 go/types 這個形式,來避免和關鍵字 type 的衝突。
避免使用其他含義的包名
要避免包名有其它的含義。例如,前面的文章中我們的溫度轉換包最初使用了 temp 包名,雖然它沒有繼續那麼用。但這是一個糟糕的嘗試,因爲 temp 大多數情況下代表了 "temporary"。我們在一小段時間裏面使用 temperature 作爲包名,但是它太長了,並且不能說明它究竟可以做什麼。自己,它變成了 tempconv,它更短並且和 strconv 等類似。
包成員的命名
因爲對其他包成員的每個引用使用一個具體的標識符,例如 fmt.Println,描述包的成員和描述包名同樣繁雜。我們不需要在 Println 中引用格式化的概念,因爲包名 fmt 還沒有準備好。當設計一個包的時候,要考慮兩個有意義的部分如何一起工作,而只是成員名。
下面有一些例子:
bytes.Equal flag.Int http.Get json.Marshal
我們可以識別出一些通用的命名模式。strings 包提供了一系列操作字符串的獨立函數。
package strings
func Index(needle, haystack string) int
type Replacer struct{ /* ... */ }
func NewReplacer(oldnew ...string) *Replacer
type Reader struct{ /* ... */ }
func NewReader(s string) *Reader
string 這個詞不會出現在任何名字中。客戶端通過 strings.Index、strings.Replacer 等引用它們。
其它的一些包可能描述爲單一類型包,例如 html/template 和 math/rand,這些包導出一個數據類型及其方法,通常有一個 New 命名的函數用來創建實例。
package rand // "math/rand"
type Rand struct{ /* ... */ }
func New(source Source) *Rand
這可能造成重複,例如在 template.Template 或 rand.Rand 中,這也是爲什麼這類包名通常都比較短。
在其他極端情況下,像 net/http 這樣的包有很多的名字,但是沒有很多的結構,因爲它們執行復雜的任務。儘管有超過 20 種類型和更多的函數,但是包中最重要的成員使用最簡短的命名:Get、Post、Handle、Error、Client、Server 等。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/wjQnSo5dn8Lca7kVuAusRg