02-07 包


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
}

在上面的代碼中,我們創建了兩個函數用於計算 AreaDiagonal。矩形的面積是長和寬的乘積。矩形的對角線是長與寬平方和的平方根。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 函數可用於執行初始化任務,也可用於在開始執行之前驗證程序的正確性。

包的初始化順序如下:

  1. 首先初始化包級別(Package Level)的變量
  2. 緊接着調用 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 做了如下修改:

  1. 變量 rectLenrectWidth 從 main 函數級別移到了包級別。
  2. 添加了 init 函數。當 rectLen 或 rectWidth 小於 0 時,init 函數使用 log.Fatal 函數打印一條日志,並終止了程序。

main 包的初始化順序為:

  1. 首先初始化被導入的包。因此,首先初始化了 rectangle 包。
  2. 接着初始化了包級別的變量 rectLenrectWidth
  3. 調用 init 函數。
  4. 最后調用 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 函數,該函數檢查 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。盡管在所有代碼里,我們都沒有使用這個包,但還是成功初始化了它。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM