2-2 Go語言的包(package)
到目前為止,我們看到的 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 資料夾。
鍵入 workspacepath/bin/geometry
,執行該程式。請用你自己的 Go 工作區來替換 workspacepath
。這個命令會執行 bin
資料夾裡的 geometry
二進位制檔案。你應該會輸出 Geometrical shape properties
。
go的工作區預設為go的安裝路徑,關於工作區修改,windows下可以通過修改環境變數中GOPATH的值來實現,當然也可以通過包管理工具來進行修改
建立自定義的包
我們將組織程式碼,使得所有與矩形有關的功能都放入 rectangle
包中。
我們會建立一個自定義包 rectangle
,它有一個計算矩形的面積和對角線的函式。
屬於某一個包的原始檔都應該放置於一個單獨命名的資料夾裡。按照 Go 的慣例,應該用包名命名該資料夾。
因此,我們在 geometry 資料夾中,建立一個命名為 rectangle
的資料夾。在 rectangle
資料夾中,所有檔案都會以 package rectangle
作為開頭,因為它們都屬於 rectangle
包。
在我們之前建立的 rectangle
資料夾中,再建立一個名為 rectprops.go
的檔案,新增下列程式碼。
在上面的程式碼中,我們建立了兩個函式用於計算 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
會將浮點數截斷到小數點兩位。
匯出名字
我們將 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
函式。
// Package rectangle include rectprops.go
package rectangle
import (
"fmt"
"math"
)
/*
* init function added
*/
func init() {
fmt.Println("rectangle package initialized")
}
// Area function is calculating the area of rectangle
func Area(len, wid float64) float64 {
area := len * wid
return area
}
// Diagonal function
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
package main
import (
"fmt"
"geometry/rectangle"
"log"
)
var rectLen, rectWidth float64 = 6, 7
func init() {
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")
/*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))
}
我們對 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
。儘管在所有程式碼裡,我們都沒有使用這個包,但還是成功初始化了它。
相關文章
- Go語言基於go module方式管理包(package)GoPackage
- Go語言之包(package)管理GoPackage
- GO語言學習筆記-包結構篇 Study for Go ! Chapter eight - Package StructureGo筆記APTPackageStruct
- GO語言————6.8 閉包Go
- go package包名規範GoPackage
- Go 語言閉包詳解Go
- [06 Go語言基礎-包]Go
- go語言處理TCP拆包/粘包GoTCP
- Go 語言操作 MySQL 之 SQLX 包GoMySql
- Go 語言 context 包實踐GoContext
- Go 語言 sync 包的應用詳解Go
- 【Go】Go語言學習筆記-3-包Go筆記
- go語言reflect包使用的幾個場景Go
- GO語言————6.10 使用閉包除錯Go除錯
- GO語言————4.7 strings和strconv 包Go
- GO語言手動處理TCP粘包GoTCP
- Go語言Context包原始碼學習GoContext原始碼
- Go語言————1、初識GO語言Go
- Go語言的context包從放棄到入門GoContext
- 【Go語言學習】匿名函式與閉包Go函式
- [ gev ] Go 語言優雅處理 TCP “粘包”GoTCP
- go語言的介面Go
- Go語言的”坑“Go
- GO語言Go
- GO語言————2、GO語言環境安裝Go
- go語言與c語言的相互呼叫GoC語言
- 包羅永珍的結構體 -- 就要學習 Go 語言結構體Go
- 《Go 語言程式設計》 讀書筆記 (八) 包Go程式設計筆記
- go語言採坑:閉包共享變數問題Go變數
- Go_go語言初探Go
- 【Go 語言入門專欄】Go 語言的起源與發展Go
- Go語言版本的forgeryGo
- Go語言的那些坑Go
- Go語言的前景分析Go
- 【Go語言入門系列】(八)Go語言是不是面嚮物件語言?Go物件
- go 語言常量Go
- Go語言mapGo
- go 語言切片Go