2.2 包
在很多編程語言中,為了封裝和隔離代碼,同時也為了代碼復用,都有包或者命名空間的語法要素,例如C#語言中的namespace以及Java語言中的package。Go語言中包的作用和其他語言中的庫或者模塊作用類似。
在Go語言里,包是一個非常重要的概念。它的設計理念是使用包來封裝和隔離不同的功能。這樣能夠更好地復用代碼,并對每個包內數據和方法的使用有更好的控制。
當包進行命名時,建議按如下規范進行命名:
· 包名應該全部小寫(大寫也不會報錯),不建議包含大寫或下劃線來命名。
· 包名應該簡短且簡潔,且能代表該包的主要功能。
· 包名不用復數形式,例如net/url,而不是net/urls。
· 包名盡量不要和標準庫中的包名重名,防止導入包的時候需要重命名。
Go語言中包的命名規范中有關于包名全部小寫的規定,可以更快地進行輸入,而不用進行大小寫切換。包是Go程序的基本單位,在Go語言中的聲明語法如下:
package 包名
包名告訴Go編譯器,當前文件屬于哪個包。一般來說,Go語言包的源代碼存放在一個根目錄中,其中包含一個或者多個.go文件。這些.go文件按照目錄進行分組并構建出上下級的層級結構。每組.go文件被稱為包。Go語言的每一個.go文件的package聲明前面需要用一段文字對該文件的作用進行描述。
所有的.go文件除了包的注釋和空行外,第一行都應該對包進行聲明。每個包都在一個單獨的目錄中,但不能將多個包放在同一個目錄中,也不能將同一個包中的文件分散到不同的目錄中。換句話說,同一個目錄中的所有.go文件必須屬于同一個包名,否則報錯,如圖2.1所示。

圖2.1 同一目錄下包含2個不同的包名
如果執行go build .命令,則會拋出無法加載包的錯誤(can't load package),并提示在不同的文件中找到多個包名,即在main.go文件中找到main包,而在test.go文件中找到demo2包。
如果當前編寫的Go程序要作為一個可執行的程序,那么必須包含一個main包(main包具有特殊意義,會作為可執行程序的入口)和一個main函數。聲明main包的文件在編譯時會使用所在目錄的目錄名作為生成文件的文件名。
注意
go build或者go install命令生成的文件,對于可執行文件,在Windows系統上是文件夾名.exe;對于UNIX、Linux或者Mac OS X系統,則為文件夾名;對于非可執行文件,可生成庫文件,為文件夾名.a。
為了更加直觀地理解Go語言包的編譯過程,我們給出一個不包含main包和main函數的文件,如示例程序2-1所示。
示例程序2-1 Go庫文件的用法:chapter02\code01\code01.go

在示例程序2-1中,注意該文件的文件名為test.go、目錄名為code01,包名和目錄名一致(為code01)。Go語言并不關心文件名,而是關心目錄名和包名。第05行的func關鍵詞聲明了一個Hello函數,注意這個函數的首字母是大寫的,這樣對包外是可見的,可以直接調用。
打開“命令提示符”窗口(在目錄go.introduce/chapter02/code01中),執行如下命令:
go install .\code01.go
或者
go install go.introduce/chapter02/code01
如果正確執行,就會在%GOPATH%\pkg\windows_amd64\go.introduce\chapter02目錄(注意,具體目錄會根據不同的環境有所差異)中生成一個code01.a文件,如圖2.2所示。

圖2.2 code01庫文件生成
注意
go install命令可能需要配置GOBIN環境變量,如C:\GoWork\bin,否則報錯。
2.2.1 包的導入
Go語言中的包和實際的代碼目錄結構一致。Go中的包在外部使用時,首先需要導入。導入包時用的是包的導入路徑。Go語言的包名和包的導入路徑是不一樣的。
在Go程序中,每一個包通過唯一的字符串(例如go.introduce/chapter02/code01)作為標識符,這個標識符就是包的導入路徑。一個包的導入路徑本質是一個目錄,該目錄中包含了構成包的一個或者多個Go源代碼文件。
按照約定,包名匹配導入路徑的最后一個目錄名。例如,包的導入路徑為go.introduce/chapter02/code01,包名就為code01。導入的包名需要使用雙引號括起來。包名的路徑是相對路徑,是從%GOPATH%/src/后開始計算的,使用符號“/”作為路徑中的分隔符。
注意
Go程序中如果導入了一個后續沒有使用的包,那么編譯會報錯。另外,如果覺得導入的包名太長,那么可以給導入的包名另起一個短名字,從而便于書寫。
對于Go語言中的包,使用關鍵字import導入,其語法為:
import 包路徑
(1)導入多個包時,如果發現包名重名了,那么需要對導入的包進行重命名,其語法為:
import 別名 包路徑
(2)導入多個包時,可以用括號一次性導入多個包,其語法如下:
import ( 包1路徑 包2路徑 ... )
(3)有時候會采用如下方式導入包,別名是點(.)或者下劃線(_):
import ( . 包路徑 _ 包路徑 )
這個點(.)符號的含義是,在調用包的函數時,可以省略包名;下劃線(_)只用于在導入包時執行初始化操作,它并不需要使用包內的其他函數、常量等資源,而是調用了該包里面的init函數。
上面包導入的幾種模式分別代表正常模式、別名模式和簡便模式。Go編譯器會根據import語句中包的路徑,結合Go配置的環境變量GOROOT和GOPATH來查找物理磁盤上的包。例如,用import "web/api"導入一個包,編譯器就會按照如下順序進行搜索:
%GOROOT%/src/web/api %GOPATH%/src/web/api
下面給出一個導入包的示例程序2-2。
示例程序2-2 Go導入包文件:chapter02\code02\code02.go

第01行聲明了一個main包,同時第06行還有一個main函數,表明編譯器會將它編譯為一個可執行文件。第04行導入了示例程序2-1創建的包文件。由于在Go語言中包的導入路徑末尾的目錄名一般和包名一致(也可以不一致),因此可以推斷包名為code01。第08行調用包code01對外導出的函數Hello。
打開“命令提示符”窗口(在目錄go.introduce/chapter02/code02中),執行如下命令:
go run .\code02.go
輸出結果如圖2.3所示,可見程序可以成功地調用code01包中的函數Hello。

圖2.3 code02.go運行結果
在Windows系統下,如果調用go install,就會在%GOPATH%/bin下生成code02.exe文件,如圖2.4所示。

圖2.4 code02.exe生成結果
前面提到,一般Go語言中的包名和目錄名一致。其實,也可以不一致。包名和目錄名不一致時,需要注意:目錄名使用在文件層面,例如庫的安裝路徑名、庫文件名以及被導入時的路徑;包名使用在代碼層面,例如調用包的函數時。
為了更好地理解上面的這段話,參考下面包名和目錄名不一致的示例程序2-3。
示例程序2-3 Go包名和目錄名不一致的用法示例:chapter02\code03\code03.go

可以看出,code03.go文件位于code03目錄中,但是其package名稱為demo,因此二者不一致。在這種情況下,go install生成的文件名為code03.a,但是包名仍然為demo,當我們在外部進行調用時需要用demo作為前綴。為了驗證,下面給出示例程序2-4。
示例程序2-4 Go調用demo包的用法示例:chapter02\code04\code04.go

第04行導入了示例程序2-3創建的包,由于包名為demo、目錄名為code03,因此在第08行調用Hello函數時用的是包名作為前綴的調用方式demo.Hello,而不是目錄名作為前綴的調用方式code03.Hello。
2.2.2 包的嵌套
包往往和源代碼目錄對應,目錄具有上下級關系,包也不例外。在Go語言中,包可以互相嵌套。因為,對于Go語言來說,包本質上對應一個目錄,包的嵌套就類似于在目錄中創建子目錄。用Visual Studio Code打開目錄go.introduce\chapter02\code05,并創建如圖2.5所示的目錄結構。

圖2.5 code05目錄結構
其中,code05.go定義了main包,通過import "go.introduce/chapter02/code05/app"導入了app包(app.go),而app包中通過import "go.introduce/chapter02/code05/app/config"導入了config包(config.go)。從目錄上看,config包定義在app包內部,說明一個包可以定義在另一個包的內部,實現包的嵌套。
正是由于Go語言包可以實現嵌套,即使包名在不同的路徑中是同名的,也可以通過包的路徑來區分不同包。config.go腳本如示例程序2-5所示。
示例程序2-5 config.go腳本:chapter02\code05\app\config\config.go

第03行用關鍵詞var聲明了一個變量Cver。注意,這里的變量名首字母是大寫的,表示該變量在外部是可見的。第02行是在Cver變量聲明語句上一條語句,通過雙斜線“//”的注釋方式對變量Cver進行說明(關于變量和注釋會在本章后續章節進行詳細說明,這里只要了解即可)。
注意
Go的代碼規范檢測可能會用到go-lint工具,它要求對外導出的變量名寫注釋,且注釋的格式為變量名+空格+變量注釋說明。在示例程序2-5中,第02行注釋中的Cver后跟著一個空格。
變量Cver是包級別的變量,當外部導入該包時,可以直接用Cver存取該變量。app.go文件內容如示例程序2-6所示。
示例程序2-6 app.go文件內容:chapter02\code05\app\app.go

在示例程序2-6中,第03行通過import導入了config包,由于Go語言中的包采用相對路徑(相對于%GOPATH%/src/),因此config包路徑為go.introduce/chapter02/code05/app/config,而不是app/config。第06行用關鍵詞func聲明了一個GetVer函數,返回string類型的數據。第07行直接返回config包中Cver變量的值。
2.2.3 特殊的init函數
Go包中有一種特殊的init函數,是Go編譯器自動可識別的,用于一些初始化工作。函數init和main在定義時不能有任何參數和返回值。該函數只能由Go程序自動調用,不可以被外部引用。
注意
init函數可以在任意包中定義,并且可以重復定義多個。main函數只能用于main包中,且只能定義一個。如果同一個.go文件中定義多個init函數,那么調用的順序為從上到下依次執行。對于同一個包中的不同文件,會按照.go文件名從小到大的順序調用各文件中的init函數。例如,某個目錄中包含a.go和b.go,那么會先執行a.go中的init函數,再執行b.go中的init函數。
對于不同的包而言,如果不相互依賴,就會按照main包中import的順序調用其包中的init函數。如果包之間存在依賴關系,調用順序按照導入包順序的反序進行初始化。因此,Go語言包中的init調用順序示意圖如圖2.6所示。

圖2.6 init調用順序示意圖
從圖2.6可以看出,包導入的順序為main → app → config,所以初始化init函數的順序為config→ app → main。為了驗證Go語言是否按照上面的規則執行init函數,我們在Visual Studio Code中構建如圖2.7所示的目錄結構。

圖2.7 驗證init調用順序的code06目錄結構
為了更加方便地查看執行init函數的順序,下面將這些.go文件組合到一塊,如示例程序2-7所示。
示例程序2-7 驗證init函數的調用順序:chapter02\code06\main.go

示例程序2-7中是將4個.go文件合并到一起的,注意第05行和第17行導入包的時候用了空白標識符,這樣就可以調用包中的init()函數。在目錄go.introduce\chapter02\code06中打開“命令提示符”窗口,執行命令go run .\main.go,則會出現如圖2.8所示的輸出結果。

圖2.8 init()函數調用順序的運行結果
注意
Go項目中不允許出現循環導入包,即使一個包被其他多個包導入,也只會初始化一次。
- SQL Server 從入門到項目實踐(超值版)
- Spring Cloud Alibaba核心技術與實戰案例
- Redis入門指南(第3版)
- Mastering Entity Framework
- 羅克韋爾ControlLogix系統應用技術
- 精通Scrapy網絡爬蟲
- Big Data Analytics
- Scientific Computing with Scala
- Python預測分析與機器學習
- Applied Deep Learning with Python
- Beginning C# 7 Hands-On:The Core Language
- 你好!Java
- Learning iOS Penetration Testing
- AngularJS by Example
- UI設計參考手冊