這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
什麼是包?為什麼使用包?
到目前為止我們見到的 Go 程式都只有一個檔案,檔案中包含了一個main函數和幾個其他函數。在實際中這種將所有代碼都放在一個檔案裡的組織方式是不可行的。這樣的組織方式使得代碼變得無法重用和維護困難。包(package)用於解決這樣的問題。
包用於組織Go原始碼,以獲得更好的重用性和可讀性。包提供了代碼封裝的機制從而使得Go應用程式易於維護。例如,假設我們正在開發一個影像處理應用,它提供了諸像裁剪,銳利化,模糊和增色等功能。一種組織代碼的方式是將所有實現同一功能的代碼放在一個獨立的包中。例如裁剪功能可以放在一個單獨包中,而銳利化功能可以放在另一個包中。這種做法的優點是:增色功能可能需要做一些銳利化的處理,那麼增色代碼中可以簡單地匯入(我們即將討論匯入)銳利化包,使用其中提供的功能即可。這種方式使得代碼變得更容易重用。
我們將逐步建立一個計算矩形面積和對角線的應用程式。
通過構建該程式,我們將更好的理解包。
main函數與main包
每個可執行檔Go程式都必須包含一個 main
函數。這個函數是執行程式的進入點。main
函數應該包含在 main
包中。
指定一個特定源檔案屬於一個包的文法為:package packagename
,這條語句應該放在源檔案的第一行。
下面讓我們開始建立 main
函數和 main
包。在 [工作空間目錄]/src
目錄下建立一個子目錄,命名為 geometry
。在該目錄下建立 geometry.go
。
編寫 geometry.go
代碼如下:
//geometry.gopackage main import "fmt"func main() { fmt.Println("Geometrical shape properties")}
package main
這一行指定了該檔案屬於 main
包。import "packagename"
語句用來匯入一個包,這裡我們匯入 fmt
包,該包匯出了 Println
方法(譯者註:該方法用於列印文本到標準輸出)。然後是 main
函數,在這裡僅列印 Geometrical shape properties
。
執行 go install geometry
編譯上面的程式。該命令在 geometry
目錄下尋找包含 main
函數的檔案,在這裡就是 geometry.go
。找到後編譯該檔案並在 [工作空間目錄]/bin
目錄下產生二進位檔案geometry
(在Windows下是 geometry.exe
)。 現在的目錄結構如下所示:
src/ geometry/ geometry.gobin/ geometry
執行 [工作空間目錄]/bin/geometry
運行該程式,其中 [工作空間目錄] 需要換成自己的實際目錄。這條命令運行在 bin
目錄下的 geometry
二進位檔案。你應該可以看到如下輸出:
Geometrical shape properties
建立自訂包
下面我們將建立一個 rectangle
包,將與矩形相關的功能(計算矩形的面積和對角線)都放在這個包裡。
屬於同一個包的源檔案應該放在獨立的檔案夾中,按照Go的慣例,該檔案夾的名字應該與包名相同。
因此讓我們在 geometry
目錄下建立一個 rectangle
子目錄。所有放在該目錄下的源檔案都應該以 package rectangle
開頭,用以表示這些源檔案都屬於 rectangle
包。
在 rectangle
目錄下建立 rectangle.go
,編寫如下代碼:
//rectprops.gopackage rectangleimport "math"func Area(len, wid float64) float64 { area := len * wid return area}func Diagonal(len, wid float64) float64 { diagonal := math.Sqrt((len * len) + (wid * wid)) return diagonal}
在上面的代碼中我們實現了兩個函數 Area
和 Diagonal
分別用於計算矩形的面積和對角線。矩形的面積為長與寬的積。矩形的對角線為長與寬的平方和再開根號。這裡調用 math
包中的 Sqrt
函數來計算平方根。
注意上面實現的兩個函數的函數名 Area
和 Diagonal
都是以大寫字母開頭的。這是必要的,我們將很快解釋為什麼需要這樣做。
匯入自訂包
為了使用自訂包我們必須先匯入它。用來匯入自訂包的文法為:import path
。我們必須指定 path
為相對於 [工作空間目錄]/src
目錄的相對路徑。我們當前的目錄結構如下所示:
src/ geometry/ geometry.go rectangle/ rectangle.go
語句 import "geometry/rectangle"
表示我們要匯入 rectangle 包。
在 geometry.go
中添加如下代碼:
//geometry.gopackage main import ( "fmt" "geometry/rectangle" //importing custom package)func main() { var rectLen, rectWidth float64 = 6, 7 fmt.Println("Geometrical shape properties") /*Area function of rectangle package used */ fmt.Printf("area of rectangle %.2f\n", rectangle.Area(rectLen, rectWidth)) /*Diagonal function of rectangle package used */ fmt.Printf("diagonal of the rectangle %.2f ",rectangle.Diagonal(rectLen, rectWidth))}
上面的代碼匯入了 rectangle
包並且使用 Area
和 Diagonal
Function Compute矩形的面積和對角線。在 Printf
中的 %.2f
格式化指示符表示僅保留浮點數的兩位小數。程式輸出如下:
Geometrical shape properties area of rectangle 42.00 diagonal of the rectangle 9.22
匯出名字
我們將 rectangle
包中的兩個函數名稱 Area
和 Diagonal
的首字母大寫,這在 Go 中有特殊的意義。在 Go 中,任何以大寫字母開頭的變數名、函數名都是被匯出的名字(exported name)。只有被匯出的名字才能被其它包訪問。在這裡我們需要在 main
包中訪問 Area
和 Diagonal
函數,因此將它們的首字母大寫。
如果將 rectprops.go
中的 Area(len, wid float64)
改成 area(len, wid float64)
,並且將 geometry.go
中的 rectangle.Area(rectLen, rectWidth)
改成 rectangle.area(rectLen, rectWidth)
,那麼運行程式時編譯器將會報錯:geometry.go:11: cannot refer to unexported name rectangle.area
。因此,如果想訪問包外的函數,必須將其首字母大寫。
init 函數
每一個包都可以包含一個 init
函數。該函數不應該有任何參數和傳回值,並且在我們的代碼中不能顯式調用它。init
函數形式如下:
func init() { }
init
函數可用於執行初始化任務,也可用於在執行開始之前驗證程式的正確性。
一個包的初始化順序如下:
- 包層級的變數首先被初始化
- 接著
init
函數被調用。一個包可以有多個 init
函數(在一個或多個檔案中),它們的調用順序為編譯器解析它們的順序。
如果一個包匯入了另一個包,被匯入的包先初始化。
儘管一個包可能被包含多次,但是它只被初始化一次。
下面讓我們對我們的程式做一些修改來理解 init
函數。
首先在 rectprops.go
中添加一個 init
函數:
//rectprops.gopackage rectangleimport "math" import "fmt"/* * init function added */func init() { fmt.Println("rectangle package initialized")}func Area(len, wid float64) float64 { area := len * wid return area}func Diagonal(len, wid float64) float64 { diagonal := math.Sqrt((len * len) + (wid * wid)) return diagonal}
我們添加了一個簡單的 init
函數,它僅列印:rectangle package initialized
。
現在我們來修改 main
包。我們知道矩形的 length
和 width
應該大於 0
。我們將在 geometry.go
中添加 init
函數和包層級的變數來做此檢查。
修改 geometry.go
如下:
//geometry.gopackage main import ( "fmt" "geometry/rectangle" //importing custom package "log")/* * 1. package variables*/var rectLen, rectWidth float64 = 6, 7 /**2. init function to check if length and width are greater than zero*/func init() { println("main package initialized") if rectLen < 0 { log.Fatal("length is less than zero") } if rectWidth < 0 { log.Fatal("width is less than zero") }}func main() { fmt.Println("Geometrical shape properties") fmt.Printf("area of rectangle %.2f\n", rectangle.Area(rectLen, rectWidth)) fmt.Printf("diagonal of the rectangle %.2f ",rectangle.Diagonal(rectLen, rectWidth))}
我們對 geometry.go
做了如下修改:
- rectLen 和 rectWidth 變數從
main
函數中移到了外面,成為了包層級的變數。
- 添加
init
函數。當 rectLen
或 rectWidth
小於 0
時,該函數利用 log.Fatal 列印一條日誌並終止程式。
main
包的初始化順序為:
- 首先初始化被匯入的包。因此
rectangle
包先被初始化。
- 然後初始化包層級的變數: rectLen 和 rectWidth 。
- init 函數被調用。
- 最後
main
函數被調用。
運行該程式,輸出如下:
rectangle package initialized main package initialized Geometrical shape properties area of rectangle 42.00 diagonal of the rectangle 9.22
正如預期的那樣,rectangle
包的 init
函數首先被調用,接著是包層級的變數 rectLen 和 rectWidth 被初始化,接著是 main
包的 init
函數被調用,該函數檢測 rectLen
和 rectWidth
是否小於 0
,如果小於 0
,則終止程式。我們將會在單獨的教程裡介紹 if
語句。現在你可以假定 if rectLen < 0
將會檢測 rectLen
是否小於 0
,如果是,則終止程式。rectWidth
也是同樣的處理。也就是說兩個條件都為假程式才繼續執行。最後,main
函數被調用。
讓我們再次修改程式來學習 init
函數的使用。
將 geometry.go
中的 var rectLen, rectWidth float64 = 6, 7
這一行改為 var rectLen, rectWidth float64 = -6, 7
。這裡將 rectLen
改為負值。
現在運行程式,得到如下結果:
rectangle package initialized main package initialized 2017/04/04 00:28:20 length is less than zero
像上面一樣, rectangle
包首先被初始化,然後是 main
包中的包層級變數 rectLen
和 rectWidth
初始化。接著調用 main
包的 init
函數,因為 rectLen
是小於 0
的,因此程式列印 length is less than zero
後退出。
代碼可以在 github 上下載。
使用空指示符
在 Go 中只匯入包卻不在代碼中使用它是非法的。如果你這麼做了,編譯器會報錯。這樣做的原因是為了避免引入過多未使用的包而導致編譯時間的顯著增加。將 geometry.go
中的代碼替換為如下代碼:
//geometry.gopackage main import ( "geometry/rectangle" //importing custom package)func main() {}
上面的程式將會報錯:geometry.go:6: imported and not used: "geometry/rectangle"
但是在開發過程中,匯入包卻不立即使用它是很常見的。可以用空指示符(_
)來處理這種情況。
下面的代碼可以避免拋出上面的錯誤:
package mainimport ( "geometry/rectangle" )var _ = rectangle.Area //error silencerfunc main() {}
var _ = rectangle.Area
這一行屏蔽了錯誤。我們應該跟蹤這些“錯誤消音器”(error silencer), 在開發結束時,我們應該去掉這些“錯誤消音器”,並且如果沒有使用相應的包,這些包也應該被一併移除。因此,建議在 import
語句之後的包層級中寫“錯誤消音器”。
有時我們匯入一個包只是為了確保該包初始化的發生,而我們不需要使用包中的任何函數或變數。例如,我們也許需要確保 rectangle
包的 init
函數被調用而不打算在代碼中的任何地方使用這個包。空指示符仍然可以處理這種情況,像下面的代碼一樣:
package main import ( _ "geometry/rectangle" )func main() {}
運行上面的程式,將會得到輸出:rectangle package initialized
。我們成功地初始化了這個包,即使在代碼中的任何地方都沒有使用它。