7. 包
什么是包,為什么使用包?
到目前為止,我們看到的 Go 程序都只有一個文件,文件里包含一個 main 函數和幾個其他的函數。在實際中,這種把所有源代碼編寫在一個文件的方法並不好用。以這種方式編寫,代碼的重用和維護都會很困難。而包(Package)解決了這樣的問題。
包用於組織 Go 源代碼,提供了更好的可重用性與可讀性。由於包提供了代碼的封裝,因此使得 Go 應用程序易於維護。
例如,假如我們正在開發一個 Go 圖像處理程序,它提供了圖像的裁剪、銳化、模糊和彩色增強等功能。一種組織程序的方式就是根據不同的特性,把代碼放到不同的包中。比如裁剪可以是一個單獨的包,而銳化是另一個包。這種方式的優點是,由於彩色增強可能需要一些銳化的功能,因此彩色增強的代碼只需要簡單地導入(我們會在隨后討論)銳化功能的包,就可以使用銳化的功能了。這樣的方式使得代碼易於重用。
我們會逐步構建一個計算矩形的面積和對角線的應用程序。
通過這個程序,我們會更好地理解包。
main 函數和 main 包
所有可執行的 Go 程序都必須包含一個 main 函數。這個函數是程序運行的入口。main 函數應該放置於 main 包中。
package packagename 這行代碼指定了某一源文件屬於一個包。它應該放在每一個源文件的第一行。
下面開始為我們的程序創建一個 main 函數和 main 包。在 Go 工作區內的 src 文件夾中創建一個文件夾,命名為 geometry。在 geometry
文件夾中創建一個 geometry.go
文件。
在 geometry.go 中編寫下面代碼。
// geometry.go
package 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
。接下來,它編譯並產生一個名為 geometry
(在 windows 下是 geometry.exe
)的二進制文件,該二進制文件放置於工作區的 bin 文件夾。現在,工作區的目錄結構會是這樣:
src
geometry
gemometry.go
bin
geometry
鍵入 workspacepath/bin/geometry
,運行該程序。請用你自己的 Go 工作區來替換 workspacepath
。這個命令會執行 bin 文件夾里的 geometry
二進制文件。你應該會輸出 Geometrical shape properties
。
創建自定義的包
我們將組織代碼,使得所有與矩形有關的功能都放入 rectangle
包中。
我們會創建一個自定義包 rectangle
,它有一個計算矩形的面積和對角線的函數。
屬於某一個包的源文件都應該放置於一個單獨命名的文件夾里。按照 Go 的慣例,應該用包名命名該文件夾。
因此,我們在 geometry
文件夾中,創建一個命名為 rectangle
的文件夾。在 rectangle
文件夾中,所有文件都會以 package rectangle
作為開頭,因為它們都屬於 rectangle 包。
在我們之前創建的 rectangle 文件夾中,再創建一個名為 rectprops.go
的文件,添加下列代碼。
// rectprops.go
package rectangle
import "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
。我們必須指定自定義包相對於工作區內 src
文件夾的相對路徑。我們目前的文件夾結構是:
src
geometry
geometry.go
rectangle
rectprops.go
import "geometry/rectangle"
這一行會導入 rectangle 包。
在 geometry.go
里面添加下面的代碼:
// geometry.go
package main
import (
"fmt"
"geometry/rectangle" // 導入自定義包
)
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 函數,得到矩形的面積和對角線。Printf 內的格式說明符 %.2f
會將浮點數截斷到小數點兩位。應用程序的輸出為:
Geometrical shape properties
area of rectangle 42.00
diagonal of the rectangle 9.22
導出名字(Exported Names)
我們將 rectangle 包中的函數 Area 和 Diagonal 首字母大寫。在 Go 中這具有特殊意義。在 Go 中,任何以大寫字母開頭的變量或者函數都是被導出的名字。其它包只能訪問被導出的函數和變量。在這里,我們需要在 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 函數不應該有任何返回值類型和參數,在我們的代碼中也不能顯式地調用它。init 函數的形式如下:
func init() {
}
init 函數可用於執行初始化任務,也可用於在開始執行之前驗證程序的正確性。
包的初始化順序如下:
- 首先初始化包級別(Package Level)的變量
- 緊接着調用 init 函數。包可以有多個 init 函數(在一個文件或分布於多個文件中),它們按照編譯器解析它們的順序進行調用。
如果一個包導入了另一個包,會先初始化被導入的包。
盡管一個包可能會被導入多次,但是它只會被初始化一次。
為了理解 init 函數,我們接下來對程序做了一些修改。
首先在 rectprops.go
文件中添加了一個 init 函數。
// rectprops.go
package rectangle
import "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 包。我們知道矩形的長和寬都應該大於 0,我們將在 geometry.go
中使用 init 函數和包級別的變量來檢查矩形的長和寬。
修改 geometry.go
文件如下所示:
// geometry.go
package main
import (
"fmt"
"geometry/rectangle" // 導入自定義包
"log"
)
/*
* 1. 包級別變量
*/
var rectLen, rectWidth float64 = 6, 7
/*
*2. init 函數會檢查長和寬是否大於0
*/
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 時,init 函數使用 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,如果條件為真,則終止程序。我們會在單獨的教程里深入學習 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。rectLen 為負數,因此當運行 init 函數時,程序在打印 length is less than zero
后終止。
使用空白標識符(Blank Identifier)
導入了包,卻不在代碼中使用它,這在 Go 中是非法的。當這么做時,編譯器是會報錯的。其原因是為了避免導入過多未使用的包,從而導致編譯時間顯著增加。將 geometry.go
中的代碼替換為如下代碼:
// geometry.go
package main
import (
"geometry/rectangle" // 導入自定的包
)
func main() {
}
上面的程序將會拋出錯誤 geometry.go:6: imported and not used: "geometry/rectangle"
。
然而,在程序開發的活躍階段,又常常會先導入包,而暫不使用它。遇到這種情況就可以使用空白標識符 _
。
下面的代碼可以避免上述程序的錯誤:
package main
import (
"geometry/rectangle"
)
var _ = rectangle.Area // 錯誤屏蔽器
func main() {
}
var _ = rectangle.Area
這一行屏蔽了錯誤。我們應該了解這些錯誤屏蔽器(Error Silencer)的動態,在程序開發結束時就移除它們,包括那些還沒有使用過的包。由此建議在 import 語句下面的包級別范圍中寫上錯誤屏蔽器。
有時候我們導入一個包,只是為了確保它進行了初始化,而無需使用包中的任何函數或變量。例如,我們或許需要確保調用了 rectangle 包的 init 函數,而不需要在代碼中使用它。這種情況也可以使用空白標識符,如下所示。
package main
import (
_ "geometry/rectangle"
)
func main() {
}
運行上面的程序,會輸出 rectangle package initialized
。盡管在所有代碼里,我們都沒有使用這個包,但還是成功初始化了它。