[06 Go語言基礎-包]
包
什麼是包,為什麼使用包?
到目前為止,我們看到的 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
。儘管在所有程式碼裡,我們都沒有使用這個包,但還是成功初始化了它。
// 1 定義一個包,新建一個資料夾,在資料夾下建go檔案,但是所有的go檔案都屬於同一個包
// 2 包名儘量用資料夾名字
// 3 大寫字母開頭表示匯出,可以在其他包中使用,匯入使用即可
// 4 包內部(屬於同一個包)的變數和函式,直接使用
// go path模式,匯入包,匯入路徑是 gopath路徑下的src資料夾開始計算
-使用go path模式,所有程式碼必須放在gopath的src路徑下,否則不能執行
-go get 下載第三方模組,下載完後,原始碼會放在gopath的src路徑下
-go get -u github.com/go-redis/redis
// go mod 模式(都用這種)
-專案路徑下會有個 go.mod (類似於python的reqirements.txt)
-程式碼可以放在任意路徑下
-匯入包路徑,從專案路徑開始,包含專案路徑
-go get 下載第三方模組
- go env -w GOPROXY=https://goproxy.cn