《Go語言程序設計》 讀書筆記 (八) 包

Go語言有超過100個的標準包(能夠用go list std | wc -l命令查看標準包的具體數目),標準庫爲大多數的程序提供了必要的基礎構件。在Go的社區,有不少成熟的包被設計、共享、重用和改進,目前互聯網上已經發布了很是多的Go語言開源包,它們能夠經過 http://godoc.org 檢索。在本章,咱們將演示若是使用已有的包和建立新的包。html

包簡介

任何包系統設計的目的都是爲了簡化大型程序的設計和維護工做,經過將一組相關的特性放進一個獨立的單元以便於理解和更新,在每一個單元更新的同時保持和程序中其它單元的相對獨立性。這種模塊化的特性容許每一個包能夠被其它的不一樣項目共享和重用,在項目範圍內、甚至全球範圍統一地分發和複用。mysql

每一個包通常都定義了一個不一樣的命名空間用於它內部的每一個標識符的訪問。每一個命名空間關聯到一個特定的包,讓咱們給類型、函數等選擇簡短明瞭的名字,這樣能夠在咱們使用它們的時候減小和其它部分名字的衝突。git

每一個包還經過控制包內名字的可見性和是否導出來實現封裝特性。經過限制包成員的可見性並隱藏包API的具體實現,將容許包的維護者在不影響外部包用戶的前提下調整包的內部實現。經過限制包內變量的可見性,還能夠強制用戶經過某些特定函數來訪問和更新內部變量,這樣能夠保證內部變量的一致性和併發時的互斥約束。github

當咱們修改了一個源文件,咱們必須從新編譯該源文件對應的包和全部依賴該包的其餘包。即便是從頭構建,Go語言編譯器的編譯速度也明顯快於其它編譯語言。Go語言的閃電般的編譯速度主要得益於三個語言特性。第一點,全部導入的包必須在每一個文件的開頭顯式聲明,這樣的話編譯器就沒有必要讀取和分析整個源文件來判斷包的依賴關係。第二點,禁止包的環狀依賴,由於沒有循環依賴,包的依賴關係造成一個有向無環圖,每一個包能夠被獨立編譯,並且極可能是被併發編譯。第三點,編譯後包的目標文件不只僅記錄包自己的導出信息,目標文件同時還記錄了包的依賴關係。所以,在編譯一個包的時候,編譯器只須要讀取每一個直接導入包的目標文件,而不須要遍歷全部依賴的的文件(不少都是重複的間接依賴)。golang

導入路徑

每一個包是由一個全局惟一的字符串所標識的導入路徑來定位的。sql

import (
    "fmt"
    "math/rand"
    "encoding/json"

    "golang.org/x/net/html"

    "github.com/go-sql-driver/mysql"
)

Go語言的規範並無指明包的導入路徑字符串的具體含義,導入路徑的具體含義是由構建工具來解釋的。數據庫

若是你計劃分享或發佈包,那麼導入路徑必須是全球惟一的。爲了不衝突,全部非標準庫包的導入路徑建議以所在組織的互聯網域名爲前綴;並且這樣也有利於包的檢索。例如,上面的import語句導入了Go團隊維護的HTML解析器和一個流行的第三方維護的MySQL驅動。json

包聲明

在每一個Go源文件的開頭都必須有包聲明語句。包聲明語句的主要目的是肯定當前包被其它包導入時默認的包名。併發

例如,math/rand包的每一個源文件的開頭都包含package rand包聲明語句,因此當你導入這個包,你就能夠用rand.Int、rand.Float64相似的方式訪問包的成員。模塊化

一般來講,默認的包名就是包導入路徑名的最後一段,所以即便兩個包的導入路徑不一樣,它們依然可能有一個相同的包名。例如,math/rand包和crypto/rand包的包名都是rand。稍後咱們將看到如何同時導入兩個有相同包名的包。

關於默認包名通常採用導入路徑名的最後一段的約定也有三種例外狀況:

第一個例外,包對應一個可執行程序,也就是main包,這時候main包自己的導入路徑是可有可無的。名字爲main的包是給go build構建命令一個信息,這個包編譯完以後必須調用鏈接器生成一個可執行程序。

第二個例外,包所在的目錄中可能有一些文件名是以test.go爲後綴的Go源文件,而且這些源文件聲明的包名也是以_test爲後綴名的。這種目錄能夠包含兩種包:一種普通包,加一種則是測試的外部擴展包。全部以_test爲後綴包名的測試外部擴展包都由go test命令獨立編譯,普通包和測試的外部擴展包是相互獨立的。測試的外部擴展包通常用來避免測試代碼中的循環導入依賴。

第三個例外,一些依賴版本號的管理工具會在導入路徑後追加版本號信息,例如"gopkg.in/yaml.v2"。這種狀況下包的名字並不包含版本號後綴,而是yaml。

導入包

能夠在一個Go語言源文件包聲明語句以後,其它非導入聲明語句以前,包含零到多個導入包聲明語句。每一個導入聲明能夠單獨指定一個導入路徑,也能夠經過圓括號同時導入多個導入路徑。下面兩個導入形式是等價的,可是第二種形式更爲常見。

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"
    mrand "math/rand" // alternative name mrand avoids conflict
)

導入包的重命名隻影響當前的源文件。其它的源文件若是導入了相同的包,能夠用導入包本來默認的名字或重命名爲另外一個徹底不一樣的名字。

導入包重命名是一個有用的特性,它不只僅只是爲了解決名字衝突。若是導入的一個包名很笨重,特別是在一些自動生成的代碼中,這時候用一個簡短名稱會更方便。選擇用簡短名稱重命名導入包時候最好統一,以免包名混亂。選擇另外一個包名稱還能夠幫助避免和本地普通變量名產生衝突。例如,若是文件中已經有了一個名爲path的變量,那麼咱們能夠將"path"標準包重命名爲pathpkg。

每一個導入聲明語句都明確指定了當前包和被導入包之間的依賴關係。若是遇到包循環導入的狀況,Go語言的構建工具將報告錯誤。

匿名包導入

若是隻是導入一個包而並不使用導入的包將會致使一個編譯錯誤。可是有時候咱們只是想利用導入包而產生的反作用:它會計算包級變量的初始化表達式和執行導入包的init初始化函數。這時候咱們須要抑制「unused import」編譯錯誤,咱們能夠用下劃線_來重命名導入的包。像往常同樣,下劃線_爲空白標識符,並不能被訪問。

import _ "image/png" // 註冊 PNG 解碼器

這個被稱爲包的匿名導入。

標準庫提供了GIF、PNG和JPEG等格式圖像的解碼器,用戶也能夠提供本身的解碼器,可是爲了保持程序體積較小,不少解碼器並無被所有包含,除非是明確須要支持的格式。image.Decode函數在解碼時會依次查詢支持的格式列表。每一個格式解碼器包的入口指定了四件事情:格式的名稱;一個用於描述這種圖像格式類型的字符串,用於解碼器檢測識別;一個Decode函數用於完成解碼圖像工做;一個DecodeConfig函數用於解碼圖像的大小和顏色空間的信息。每一個解碼器包的入口經過調用image.RegisterFormat函數註冊解碼器,通常是在每一個格式包的init初始化函數中調用,例如image/png包是這樣註冊的:

package png // image/png

func Decode(r io.Reader) (image.Image, error)
func DecodeConfig(r io.Reader) (image.Config, error)

func init() {
    const pngHeader = "\x89PNG\r\n\x1a\n"
    image.RegisterFormat("png", pngHeader, Decode, DecodeConfig)
}

若是沒有這一行匿名導入語句,程序依然能夠編譯和運行,可是它將不能正確識別和解碼PNG格式的圖像。

數據庫包database/sql也是採用了相似的技術,讓用戶能夠根據本身須要選擇導入必要的數據庫驅動。例如:

import (
    "database/sql"
    _ "github.com/lib/pq"              // enable support for Postgres
    _ "github.com/go-sql-driver/mysql" // enable support for MySQL
)

db, err = sql.Open("postgres", dbname) // OK
db, err = sql.Open("mysql", dbname)    // OK
db, err = sql.Open("sqlite3", dbname)  // returns error: unknown driver "sqlite3"

包的命名

下面是一些關於Go語言軟件包和包成員命名的約定。

當建立一個包,通常要簡潔明瞭的包名,但也不能太簡短致使難以理解。標準庫中最經常使用的包有bufio、bytes、flag、fmt、http、io、json、os、sort、sync和time等包,它們的名字都簡潔明瞭。

要儘可能避免包名與常常用於局部變量的名字發生衝突,不然可能致使用戶重命名導入包,例如前面看到的path包。

包名通常採用單數的形式。標準庫的bytes、errors和strings使用了複數形式,這是爲了不和預約義的類型衝突,一樣還有go/types是爲了不和type關鍵字衝突。

要避免包名有其它的含義。例如,2.5節中咱們的溫度轉換包最初使用了temp包名,雖然並無持續多久。但這是一個糟糕的嘗試,由於temp幾乎是臨時變量的同義詞。而後咱們有一段時間使用了temperature做爲包名,可是這個名字並無表達包的真實用途。最後咱們改爲了和strconv標準包相似的tempconv包名,這個名字比以前的就好多了。

當設計一個包的時候,須要考慮包名和成員名兩個部分如何很好地配合。成員名沒必要再包含包名,下面有一些例子:

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.Indexstrings.Replacer等。

相關文章
相關標籤/搜索