原文:CalliCoder — A beginners guide to Packages in Golang
Go 旨在鼓勵良好的軟體工程實踐。高品質軟體的指導原則之一是 DRY 原則:Don’t Repeat Yourself,這意味著你永遠不應重複撰寫相同的程式碼。你應該重複使用並儘可能建立在現有程式碼上。
允許程式碼重複使用的最基本的建構區塊是函數。Package 是程式碼可重複使用性的下一步。他們可幫助你將相關的 Go 原始檔組織到同一單元中,使他們模組化、可重複使用和可維護。
在本文中,你將學到如何將 Go 程式碼組織到可重複使用的 package 中,如何匯入 package,如何將函數、型別或變數匯出到外部 package,以及如何安裝第三方 package。
讓我們開始吧!
Go 工作區及程式碼組織
在學習 Go package 之前,了解 Go 程式碼在所謂的 Go 工作區中的組織方式非常重要。請查看以下文章,以複習有關 Go 組織程式碼的概念:
安裝、設定 Golang 的 GOPATH 和 Go 工作區
Go Package
基本上來說,package 不過是 Go 工作區中包含一個或多個 Go 原始檔案或其他 Go package 的目錄。
每個 Go 原始檔案都隸屬於一個 package。我們使用以下語法將原始檔案宣告為 package 的一部分:
package <packagename>
上面的 package 宣告必須是 Go 原始檔案中的第一行程式碼。Go 原始檔案中定義的所有函數、型別和變數都會成為所宣告 package 的一部分。
你可以選擇將 package 中定義的成員匯出到外部 package,或保持其私有化在相同 package。其他 package 可以匯入並使用從 package 中匯出的函數或型別。
讓我們來看個範例:到目前為止,我們在本系列教學中幾乎會看到所有程式碼都包含以下這行:
import "fmt"
fmt
是一個核心函式庫,包含格式化和列印輸出或從各種 I/O 來源讀取輸入等相關的功能。它匯出如 Println()
、Printf()
、Scanf()
等函數,以供其他 package 重複使用。
以這種方式封裝功能具有以下優點:
它減少了命名衝突。你可以在不同的 package 中使用相同的函數名稱。這讓我們的函數名稱簡短明瞭。
它將相關程式碼組織在一起,以便更輕鬆地找到要重複使用的程式碼。
它僅需要重新編譯程式實際上有更改的一小部分,進而加速了編譯過程。儘管我們使用了
fmt
package,但我們不需要在每次修改我們的程式時重新編譯它。
main
package 和 main()
函數
Go 程式從 main
package 開始執行。這是一個特殊的 package,用於想要執行的程式。
按照慣例,可執行程式(有 main
package 的程式)稱為指令。其他則簡稱為 Package。
main()
函數是一個特殊的函數,它是可執行程式的進入點。讓我們來看一個 Go 的可執行程式範例:
// Package declaration
package main
// Importing packages
import (
"fmt"
"time"
"math"
"math/rand"
)
func main() {
// Finding the Max of two numbers
fmt.Println(math.Max(73.15, 92.46))
// Calculate the square root of a number
fmt.Println(math.Sqrt(225))
// Printing the value of `𝜋`
fmt.Println(math.Pi)
// Epoch time in milliseconds
epoch := time.Now().Unix()
fmt.Println(epoch)
// Generating a random integer between 0 to 100
rand.Seed(epoch)
fmt.Println(rand.Intn(100))
}
$ go run main.go
# Output
92.46
15
3.141592653589793
1538045386
40
匯入 Package
Go 中有兩種匯入 package 的方法:
// Multiple import statements
import "fmt"
import "time"
import "math"
import "math/rand"
// Factored import statements
import (
"fmt"
"time"
"math"
"math/rand"
)
Go 的慣例是:_package 的名稱與匯入路徑的最後一個元素相同_。舉例來說,匯入為 math/rand
的 package 的名稱為 rand
。它是透過 math/rand
路徑匯入的,因為它是 math
package 中的巢狀子目錄。
匯出與未匯出的名稱
以大寫字母開頭的所有內容(變數、型別或函數)都會被匯出,並可被外部 package 看見。
任何不以大寫字母開頭的內容都不會被匯出,並且僅在同一 package 中可見。
匯入 package 時,只能存取其匯出的名稱。
package main
import (
"fmt"
"math"
)
func main() {
// MaxInt64 is an exported name
fmt.Println("Max value of int64: ", int64(math.MaxInt64))
// Phi is an exported name
fmt.Println("Value of Phi (ϕ): ", math.Phi)
// pi starts with a small letter, so it is not exported
fmt.Println("Value of Pi (𝜋): ", math.pi)
}
# Output
./exported_names.go:16:38: cannot refer to unexported name math.pi
./exported_names.go:16:38: undefined: math.pi
要修正以上錯誤,需要將 math.pi
更改為 math.Pi
。
建立和管理自訂 Package
到目前為止,我們只有在 main
package 中撰寫程式碼,並使用從 Go 核心函式庫匯入的功能。
讓我們建立一個 Go 專案範例,該專案具有多個自訂 package 以及一堆原始碼檔案,並了解相同的 package 宣告、匯入和匯出概念如何套用在自訂 package。
這是我們在 Go 工作區內組織專案的方式:
src
資料夾包含我們的 Go 專案,該專案位於名為 example
的 git 儲存庫中。bin
和 pkg
資料夾分別包含可執行檔和 package 檔案。他們在我們安裝程式時由 Go 工具自動產生。
讓我們逐一查看程式中的每個原始檔案中的程式碼:
numbers/prime.go
package numbers
import "math"
// Checks if a number is prime or not
func IsPrime(num int) bool {
for i := 2; i <= int(math.Floor(math.Sqrt(float64(num)))); i++ {
if num%i == 0 {
return false
}
}
return num > 1
}
strings/reverse.go
package strings
// Reverses a string
/*
Since strings in Go are immutable, we first convert the string to a mutable array of runes ([]rune),
perform the reverse operation on that, and then re-cast to a string.
*/
func Reverse(s string) string {
runes := []rune(s)
reversedRunes := reverseRunes(runes)
return string(reversedRunes)
}
strings/reverse_runes.go
package strings
// Reverses an array of runes
// This function is not exported (It is only visible inside the `strings` package)
func reverseRunes(r []rune) []rune {
for i, j := 0, len(r)-1; i < j; i, j = i+1, j-1 {
r[i], r[j] = r[j], r[i]
}
return r
}
strings/greeting/texts.go (Nested package)
// Nested Package
package greeting
// Exported
const (
WelcomeText = "Hello, World to Golang"
MorningText = "Good Morning"
EveningText = "Good Evening"
)
// Not exported (only visible inside the `greeting` package)
var loremIpsumText = `Lorem ipsum dolor sit amet, consectetur adipiscing elit,
sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad
minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
commodo consequat.`
myapp/app.go (The main package: entry point of our program)
package main
import (
"fmt"
"github.com/callicoder/example/numbers"
"github.com/callicoder/example/strings"
"github.com/callicoder/example/strings/greeting" // Importing a nested package
str "strings" // Package Alias
)
func main() {
fmt.Println(numbers.IsPrime(19))
fmt.Println(greeting.WelcomeText)
fmt.Println(strings.Reverse("callicoder"))
fmt.Println(str.Count("Go is Awesome. I love Go", "Go"))
}
# Installing the Go package
$ go install github.com/callicoder/example/myapp
# Running the executable binary
$ myapp
true
Hello, World to Golang
redocillac
2
注意事項
匯入路徑
所有匯入路徑都相對於 Go 工作區的
src
目錄。import ( "github.com/callicoder/example/numbers" "github.com/callicoder/example/strings" "github.com/callicoder/example/strings/greeting" )
Package 別名
你可以使用 package 別名來解決相同名稱的不同 package 之間的衝突,或只是為匯入的 package 取一個簡短的名字。
import ( str "strings" // Package Alias )
巢狀 Package
你可以在 package 中放入另一個 package。就像建立子目錄一樣簡單:
src github.com/callicoder/example strings # Package greeting # Nested Package texts.go
可以如同根 package 一樣匯入巢狀 package。只要提供他相對於
$GOPATH/src
目錄的路徑:import ( "github.com/callicoder/example/strings/greeting" )
安裝第三方 Package
你可以使用 go get
指令從遠端儲存庫中取得第三方 package。
$ go get -u github.com/jinzhu/gorm
上面的指令從 Github 取得 gorm
package,並將其儲存在 Go 工作區中的 src/github.com/jinzhu/gorm
路徑中。
你現在可以像這樣在程式中匯入並使用以上 package:
import "github.com/jinzhu/gorm"
要注意的是,與其他語言和 package 管理工具(npm
、maven
等)不同,Go 沒有集中的官方 package 儲存庫。因此,他要求你在 go get
指令中提供 package 的主機名稱和路徑。
這裡有另一個 go get
指令的範例,該指令從 go.uber.org
(Uber 的 Go package 託管網站)下載 package:
$ go get -u go.uber.org/zap
上面的指令下載並儲存 zap
package 在 $GOPATH/src/go.uber.org/zap
路徑中。你可以這樣匯入它:
import "go.uber.org/zap"
結論
在本文中,你學到了如何將 Go 程式碼組織到可重複使用的 package 中、如何匯入 package、如何匯出 package 中的成員、如何建立自訂 package 以及如何安裝第三方 package。
在之後的文章中,你將會學習有關 package 初始化、空白識別字元和 package 文件。