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 源文件的開頭都需要進行包聲明。聲明該文件歸屬的包,主要的目的是當該包被其他包引入的時候作爲其默認的標識符(稱爲包名)。

注意事項:

例如,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 "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})
}

此處我們藉助之前文章中使用到的一個例子:

Go 基礎數據類型介紹(整型、浮點型、複數、布爾值)

在上面的文章中有一個浮點數繪圖的例子,我們使用這個程序(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