你需要知道的關於 Go 包的一切

SmauelL發表於2019-12-09

在 Go 語言中包管理和部署的完整概述

如果您熟悉 JavaNodeJS 等語言,那麼您可能非常熟悉。一個包只不過是一個包含一些程式碼檔案的目錄,它從一個單一的引用點公開不同的變數( 特點 )。讓我來解釋一下這是什麼意思。

假設您有上千個函式,在處理任何專案時都需要它們。其中一些函式具有共同的行為。例如,toUpperCase 和 toLowerCase 函式轉換了 stringcase,因此你可以將它們寫入單個檔案中( 可能是 case.go )。還有其他函式可以對 string 資料型別執行其他操作,因此也可以將它們寫入單獨的檔案中。

由於您有許多檔案可以處理 string 資料型別,所以您建立了一個名為 string 的目錄,並將所有與 string 相關的檔案放入其中。最後,將所有這些目錄放在一個父目錄中,該目錄將是您的包。整個包結構如下所示。

package-name
├── string
|  ├── case.go
|  ├── trim.go
|  └── misc.go
└── number
   ├── arithmetics.go
   └── primes.go

我將詳細解釋如何從包中匯入函式和變數,以及如何將所有內容混合在一起形成一個包,但是現在,將您的包想象成一個包含 .go 檔案的目錄。

每一個 Go 程式都必須是某個包的一部分。正如 開始學習 Go 這個課程所討論的,一個獨立的可執行 Go 程式必須有 package main 宣告。如果一個程式是main 包的一部分,那麼 go install 將建立一個二進位制檔案;它在執行時呼叫程式的 main 函式。如果一個程式不是 main 包的一部分,那麼使用 go install 命令建立一個 package archive 檔案。不要擔心,我將在後面的主題中解釋這些

讓我們建立一個可執行包。眾所周知,要建立一個二進位制可執行檔案,我們需要我們的程式是 main 包的一部分,它必須有 main 函式,這是執行的入口點。

包名是 src 目錄中包含的目錄名。在上面的例子中,app 是包,因為 appsrc 目錄的子目錄。因此,go install app 命令在 GOPATHsrc 目錄中查詢 app 子目錄。然後編譯包並在 bin 目錄中建立 app 的二進位制可執行檔案,因為 bin目錄在 PATH 中,所以改檔案應該可以在終端上執行。

包宣告應該是像上面例子 package main 第一行程式碼一樣,可以不同於包的名稱。因此,您可能會發現一些包的名稱(目錄的名稱)與包宣告不同。當您匯入一個包時,將使用包宣告來建立包引用變數,本文後面將對此進行解釋。

go install <package> 命令在給定的 package 目錄中查詢 main 包宣告的任何檔案。如果它找到了一個檔案,那麼Go 知道這是一個可執行程式,它需要建立一個二進位制檔案。一個包可以有許多檔案,但只有一個檔案具有 main 函式,因為該檔案將是執行的入口點。

如果一個包不包含 main 包宣告的檔案,那麼 Go 會在 pkg 目錄中建立一個 package archive (.a) 檔案。

因為,app 不是一個可執行的包,所以它在 pkg 目錄中建立 app.a 檔案。我們不能執行這個檔案,因為它不是一個二進位制檔案。

包的命名約定

Go 社群建議對包使用簡單明瞭的名稱。例如,strutils 用於 string 通用 方法或 http 用於 HTTP 請求相關函式。應該避免使用 under_scoreshy-phensmixedCaps 這類的包名。


正如我們所討論的,有兩種型別的包。一種是 可執行包,另一種是 通用包。可執行包是您的主要應用程式,因為您將執行它。通用包不是自執行的,相反,它通過提供通用函式和其他重要內容來增強可執行程式包的功能。

眾所周知,一個包只有一個目錄,讓我們在 src 中建立 greet 目錄,並在其中建立幾個檔案。這一次,我們將在每個檔案的頂部編寫一個 package greet 宣告,宣告這是一個通用包。

匯出成員

一個通用包應該為匯入它的包提供一些變數。就像 JavaScript 中的 export 語法一樣,如果一個變數名以大寫字母開頭,Go 就會匯出這個變數。所有其他不以大寫字母開頭的變數都是包的私有變數。

從現在開始,我將在本文中使用變數來描述一個匯出成員,但匯出成員可以是任何型別的,如 constantmapfunctionstructarrayslice 等。

讓我們從 day.go 檔案中匯出一個 greeting 變數

在上面的程式中,Morning 變數將從包中匯出,但 morning 變數不會被匯出,因為它以小寫字母開頭。

匯入包

現在,我們需要一個可執行包,它將使用我們的 greet 包。讓我們在 src 中建立一個 app 目錄,並建立帶有 main 包宣告和 main 函式的 entry.go 檔案。注意這裡,Go 包沒有像 Node 中 index.js輸入檔案命名系統。對於一個可執行包,一個帶有 main 函式的檔案是執行的入口檔案。

要匯入一個包,我們使用 import 語法,後面跟著包名

與其他程式語言不同,包名也可以是一個子路徑,如一個目錄/greet,Go會自動為我們解析 greet 包的路徑,如前面的巢狀包的主題所示。

首先在 GOROOT/src 目錄中搜尋包目錄,如果沒有找到包,則查詢 GOPATH/src。由於 fmt 包是位於 GOROOT/src 的 Go 標準庫的一部分,所以它是從那裡匯入的。由於 Go 在 GOROOT 中找不到 greet 包,它將在 GOPATH/src 中查詢,我們會在那裡找到它。

上面的程式丟擲編譯錯誤,因為 morning 變數在 greet 包中是私有的。如您所見,我們使用 . ( )符號來訪問從包匯出的成員。當您匯入一個包時,Go 使用包的包宣告建立一個全域性變數。在上面的例子中,greet 是 Go 建立的全域性變數,因為我們在 greet 包中包含的程式中使用了 package greet 宣告。

我們可以使用分組語法( 括號 )將 fmtgreet 包匯入在一組。這一次,我們的程式可以很好地編譯,因為 Morning 變數可以從包外部獲得。

巢狀包

我們可以將一個包嵌入到另一個包中。因為對於 Go 來說,包只是一個目錄,就像在已經存在的包中建立子目錄一樣。我們所要做的就是提供巢狀包的相對路徑。

編譯包

如前所述,go run 命令編譯並執行一個程式。我們知道,go install 命令編譯包並建立二進位制可執行檔案或包存檔檔案。這是為了避免每次編譯一個( 匯入這些包的程式 )的時候都要編譯這些包。go install 預編譯一個包 並且 Go 指的是 .a 檔案。

一般來說,當你安裝一個第三方包時,Go 編譯包並建立包存檔檔案。如果您在本地編寫了包,那麼您的 IDE 可能會在您將檔案儲存在包中或修改包時建立包存檔。如果你安裝了 Go 外掛,VSCode 會在你儲存時編譯這個包


當我們執行 Go 程式時,Go 編譯器對包、包中的檔案和包中的變數宣告遵循一定的執行順序。

包作用域

作用域是程式碼塊中的一個區域,其中定義的變數是可訪問的。包作用域是包中的一個區域,其中宣告的變數可以從包中訪問( 跨越包中的所有檔案 )。這個區域是包中檔案的最上方的部分。


看看 go run 命令。這一次,我們沒有執行一個檔案,而是使用了一個 glob 模式,將所有檔案都包含在 app 包中以供執行。Go 足夠聰明,可以找出應用程式的入口點,即 entry.go,因為它有 main 函式。我們也可以使用如下命令( 檔名順序不重要 )。

go run src/app/version.go src/app/entry.go

go installgo build 命令需要一個包名,其中包含包內的所有檔案,所以我們不必像上面那樣指定它們。

回到我們的主要問題,我們可以在包的任何地方使用在 version.go 檔案中宣告的變數 version,即使它沒有被匯出( version ),因為它是在包作用域中宣告的。如果 version 變數是在函式中宣告的,那麼它就不在包作用域內,上面的程式就無法編譯。

不允許在同一個包中重新宣告具有相同名稱的全域性變數. 因此,一旦宣告瞭 version 變數,就不能在包範圍內重新宣告它。但是你可以在其他地方重新宣告。

變數初始化

當一個變數 a 依賴於另一個變數 b 時,應事先定義 b,否則程式無法編譯。Go 在函式內部遵循這個規則。

但是當這些變數在包作用域定義時,它們在初始化週期中宣告。

在上面的例子中,首先宣告瞭 c,因為它的值已經宣告瞭。 在後面的初始化週期中,宣告瞭b,因為它依賴於c,而 c 的值已經宣告瞭。在最後的初始化週期中,a 被宣告並賦值給 b。Go 可以像下面這樣處理複雜的初始化週期。

在上面的例子中,首先宣告 c,然後宣告 b,因為它的值取決於 c,最後宣告 a,因為它的值取決於 b。您應該避免如下所示的任何初始化迴圈,如下所示,初始化進入遞迴迴圈。

包作用域的另一個例子是,在一個單獨的檔案中有一個函式 f,它從 main 檔案引用變數 c

初始化(init)函式

main 函式一樣,init 函式在包初始化時被呼叫。它不接受任何引數,也不返回任何值。 init 函式是由 Go 隱式宣告的,因此您不能從任何地方引用它( 或像 init() 那樣呼叫它)。在一個檔案或包中可以有多個 init 函式。init 函式在檔案中的執行順序將根據它們的出現順序。

你可以在包的任何地方使用 init 函式。這些 init 函式按詞法檔名順序呼叫(字母順序)。

畢竟,init 函式被執行,main 函式被呼叫。因此,init 函式的主要工作是初始化全域性變數,這些變數在全域性上下文中無法初始化。例如,初始化一個陣列。

由於 for 語法在包範圍內無效,我們可以使用 init 函式中的 for 迴圈來初始化大小為 10 的陣列 integers

包別名

當您匯入一個包時,使用包的宣告來建立一個變數。如果您匯入多個具有相同名稱的包,這將導致衝突。

// parent.go
package greet
var Message = "Hey there. I am parent."
// child.go
package greet
var Message = "Hey there. I am child."

因此,我們使用包別名。我們在 import 關鍵字和包名之間宣告一個變數名,它將成為引用包的新變數。

在上面的例子中,greet/greet 包現在被 child 變數引用。如果您注意到,我們在 greet 包中新增了一個下劃線。 下劃線是 Go 中的一個特殊字元,它充當 null 容器。 因為我們匯入了 greet 包,但是沒有使用它,所以 Go 編譯器會報錯。為了避免這種情況,我們將包的引用儲存到 _ 中,Go 編譯器將直接忽略它。

使用下劃線來別名化一個包,這看起來沒什麼用,但是當你想要初始化一個包卻不使用它的時候,這是非常有用的。

// parent.go
package greet

import "fmt"

var Message = "Hey there. I am parent."

func init() {
    fmt.Println("greet/parent.go ==> init()")
}

// child.go
package greet

import "fmt"

var Message = "Hey there. I am child."

func init() {
    fmt.Println("greet/greet/child.go ==> init()")
}

要記住的主要事情是,每個匯入的包僅初始化一次。因此,如果包中有任何匯入語句,則匯入的包在主包執行的生命週期中只初始化一次。

如果我們使用 . (點)作為別名,如 import .「greet/greet」然後所有 greet 包的匯出成員將在本地檔案塊範圍內可用,我們可以引用 Message 而不使用限定詞 child。因此,fmt.Println(Message) 可以正常工作。這種型別的匯入被叫做點匯入,然而 Go 社群 不太喜歡它,因為它會導致一些問題。


到目前為止,我們已經瞭解了關於包的一切。現在,讓我們結合我們的理解來看 Go 程式是如何初始化的。

go run *.go
├── Main package is executed
├── All imported packages are initialized
|  ├── All imported packages are initialized (recursive definition)
|  ├── All global variables are initialized 
|  └── init functions are called in lexical file name order
└── Main package is initialized
   ├── All global variables are initialized
   └── init functions are called in lexical file name order

這裡有一個小例子來證明它。

// version/get-version.go
package version
import "fmt"
func init() {
 fmt.Println("version/get-version.go ==> init()")
}
func getVersion() string {
 fmt.Println("version/get-version.go ==> getVersion()")
 return "1.0.0"
}
/***************************/
// version/entry.go
package version
import "fmt"
func init() {
 fmt.Println("version/entry.go ==> init()")
}
var Version = getLocalVersion()
func getLocalVersion() string {
 fmt.Println("version/entry.go ==> getLocalVersion()")
 return getVersion()
}
/***************************/
// app/fetch-version.go
package main
import (
 "fmt"
 "version"
)
func init() {
 fmt.Println("app/fetch-version.go ==> init()")
}
func fetchVersion() string {
 fmt.Println("app/fetch-version.go ==> fetchVersion()")
 return version.Version
}
/***************************/
// app/entry.go
package main
import "fmt"
func init() {
 fmt.Println("app/entry.go ==> init()")
}
var myVersion = fetchVersion()
func main() {
 fmt.Println("app/fetch-version.go ==> fetchVersion()")
 fmt.Println("version ===> ", myVersion)
}


安裝第三方包只是將遠端程式碼克隆到本地的 src/<package> 目錄中。不幸的是,Go 不支援包版本或提供包管理器,但有一個提議正在等待 在這裡。(注:已經實現了可以看這裡

因為 Go 沒有一個集中的官方包登錄檔,所以它要求您提供包的主機名和路徑。

$ go get -u github.com/jinzhu/gorm

上述命令從 http://github.com/jinzhu/gorm URL 匯入檔案並將其儲存在 src/github.com/jinzhu/gorm 目錄中。正如在巢狀包中討論的,您可以像下面這樣匯入 gorm 包。

package main
import "github.com/jinzhu/gorm"
// use ==> gorm.SomeExportedMember

所以,如果你製作了一個包,並希望人們使用它,只要在 GitHub 上釋出它,就可以了。如果您的包是可執行的,人們可以將它用作命令列工具,也可以將它匯入程式並將其用作實用程式模組。他們唯一需要做的就是使用下面的命令。

$ go get github.com/your-username/repo-name

譯自 medium

最初的時候也是最苦的時候,最苦的時候也是最酷的時候。

相關文章