2-2 Go語言的包(package)

Zenith_Lee發表於2020-10-02

到目前為止,我們看到的 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 的檔案,新增下列程式碼。

在上面的程式碼中,我們建立了兩個函式用於計算 AreaDiagonal。矩形的面積是長和寬的乘積。矩形的對角線是長與寬平方和的平方根。math 包下面的 Sqrt 函式用於計算平方根。

注意到函式 AreaDiagonal 都是以大寫字母開頭的。這是有必要的,我們將會很快解釋為什麼需要這樣做。

匯入自定義包

為了使用自定義包,我們必須要先匯入它。匯入自定義包的語法為 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 包,並呼叫了裡面的 AreaDiagonal 函式,得到矩形的面積和對角線。Printf 內的格式說明符 %.2f 會將浮點數截斷到小數點兩位。

匯出名字

我們將 rectangle 包中的函式AreaDiagonal 首字母大寫。在 Go 中這具有特殊意義。在 Go 中,任何以大寫字母開頭的變數或者函式都是被匯出的名字。其它包只能訪問被匯出的函式和變數。在這裡,我們需要在 main 包中訪問 AreaDiagonal 函式,因此會將它們的首字母大寫。

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 做了如下修改:

  • 變數 rectLenrectWidthmain 函式級別移到了包級別。
  • 新增了 init 函式。當 rectLenrectWidth 小於 0 時,init 函式使用 log.Fatal 函式列印一條日誌,並終止了程式。

main 包的初始化順序為:

  • 首先初始化被匯入的包。因此,首先初始化了 rectangle 包。
  • 接著初始化了包級別的變數 rectLenrectWidth
  • 呼叫 init 函式。
  • 最後呼叫 main 函式。

當執行該程式時,會有如下輸出。

rectangle package initialized
main package initialized       
Geometrical shape properties   
area of rectangle 42.00        
diagonal of the rectangle 9.22 

果然,程式會首先呼叫 rectangle 包的 init 函式,然後,會初始化包級別的變數 rectLenrectWidth。接著呼叫 main 包裡的 init 函式,該函式檢查 rectLenrectWidth 是否小於 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 包中的包級別的變數 rectLenrectWidthrectLen 為負數,因此當執行 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。儘管在所有程式碼裡,我們都沒有使用這個包,但還是成功初始化了它。

相關文章