Go 中的 import 宣告
Go 中的程式由各種包組成。通常,包依賴於其它包,這些包內置於標準庫或者第三方。包首先需要被匯入才能使用包中的匯出識別符號。這是通過結構體呼叫import 宣告 來實現的:
package main import ( "fmt" "math" ) func main() { fmt.Println(math.Exp2(10))// 1024 }
上面我們有一個 import 匯入的例子,其中包含了兩行匯入宣告。每行宣告定義了單個包的匯入。
命名為main 的包,是用來建立可執行的二進位制檔案。程式的執行是從包main 開始,通過呼叫包中也叫做main 的函式開始。
但是,還有其它一些鮮為人知的匯入用法,這些用法在各種場景下都很實用:
import ( "math" m "math" . "math" _ "math" )
這四個匯入格式都有各自不同的行為,在這篇文章中我們將分析這些差異。
匯入包只能引用匯入包中的匯出識別符號。 匯出識別符號是以Unicode大寫字母開頭的
- ofollow,noindex" target="_blank">https://golang.org/ref/spec#Exported_identifiers 。
基礎
Import 宣告剖析
ImportDeclaration = "import" ImportSpec ImportSpec= [ "." | "_" | Identifier ] ImportPath
- Identifier 是將在限定識別符號中使用的任何有效識別符號。
-
ImportPath
是一個字串(原始或解釋字串,譯註:例如
\n
和 "\n" 的區別,原始字串或回車)
讓我們看一些例子:
import . "fmt" import _ "io" import log "github.com/sirupsen/logrus" import m "math"
合併 Import 宣告
匯入兩個或者更多的包可以有兩種寫法。一個是,我們可以寫多個 import 宣告:
import "io" import "bufio"
或者,我們可以將多個 import 宣告合併(將多個匯入放在一條匯入宣告中):
import ( "io" "bufio" )
第二種匯入方式在匯入很多個包的時候非常實用,然後多次重複的用 import 關鍵字匯入包會降低可讀性。如果你不使用自動匯入之類的工具,例如:https://github.com/bradfitz/goimports ,這種方式還可以減少按鍵次數。
(短)匯入路徑
匯入規範中使用的字串文字(每個匯入宣告包含一個或多個匯入規範)告訴匯入哪個包。這個字串稱為匯入路徑。根據語言規範,它取決於如何解釋匯入路徑(字串)的實現方式,但在現實運用中它的路徑相對包的第三方庫目錄或go env GOPATH / src
目錄(更多內容參考GOPATH
)。
內建的包匯入使用 “math” 或 “fmt” 等短匯入路徑。
.go 檔案剖析
每個.go
檔案的結構是相同的。首先是 package 語句,可選地在其前面加上通常是描述包的作用的註釋。然後零個或多個匯入宣告。 接著包含零個或多個頂級宣告。
// description... package main // package clause // zero or more import declarations import ( "fmt" "strings" ) import "strconv" // top-level declarations func main() { fmt.Println(strings.Repeat(strconv.FormatInt(15, 16), 5)) }
強制組織 (Enforced organisation) 不允許引入不必要的混亂,這簡化了解析和基本的程式碼庫跳轉(匯入宣告不能放在 package 子句之前,也不能與頂級宣告交錯,所以它總是很容易找到)。
匯入作用域
匯入的作用域是檔案塊級別。這意味著它可以從整個檔案中訪問,但不能在整個包中被訪問:
// github.com/mlowicki/a/main.go package main import "fmt" func main() { fmt.Println(a) } // github.com/mlowicki/a/foo.go package main var a int = 1 func hi() { fmt.Println("Hi!") }
上述程式碼無法被成功編譯:
> go build // github.com/mlowicki/a ./foo.go:6:2: undefined: fmt
更多的關於作用域的內容參考之前發表的文章:Scopes in Go
匯入的型別
自定義包名
按照約定,匯入路徑的最後一個部分同時也是匯入包的包名。當然,我們也可以不遵循這個約定:
// github.com/mlowicki/main.go package main import ( "fmt" "github.com/mlowicki/b" ) func main() { fmt.Println(c.B) } // github.com/mlowicki/b/b.go package c var B = "b"
這個輸出很明顯是b 。當然儘可能的遵循這些約定是更好的 — 很多工具也是依賴這個約定。如果自定義包名在匯入的時候沒有特別的指定,則使用來自包子句的名稱來引用匯入包的匯出識別符號:
package main import "fmt" func main() { fmt.Println("Hi!") }
也可以自定義一個包名稱進行匯入:
// github.com/mlowicki/b/b.go package b var B = "b" // github.com/mlowicki/main.go (依據原文含義,譯者新增) package main import ( "fmt" c "github.com/mlowicki/b" ) func main() { fmt.Println(c.B) }
這個輸出結果和之前一樣。如果我們的包具有與其它包相同的介面(匯出的識別符號),則這種匯入形式非常有用。 一個這樣的例子是https://github.com/sirupsen/logrus ,它有一個與 log 相容的 API :
import log "github.com/sirupsen/logrus"
如果我們只使用內建日誌包中的 API ,那麼用匯入log
替換這樣的匯入不需要對原始碼進行任何更改。它也有點短(但仍然有意義)所以可能會節省一些按鍵次數。
匯入所有的匯出識別符號
例如:
import m "math" import "fmt"
可以使用指定的包的別名 (m.Exp) 或者匯入的包名 (fmt.Prinln) 實現引用匯出識別符號。還有另一個方式不用通過限定識別符號就可以訪問匯出識別符號:
package main import ( "fmt" . "math" ) func main() { fmt.Println(Exp2(6))// 64 }
什麼時候這種用法有用呢?在測試中。假設我們有一個包 b 匯入包 a。現在我們想給包 a 新增測試。如果測試也在包 a 中,並且測試也會匯入包 b (因為到時需要在那實現一些東西),那麼我們將最終將會變成迴圈依賴,這是禁止的。繞過它的一種方法是將測試放入單獨的包中,如 a_tests。然後我們需要匯入包 a 並使用限定識別符號引用每個匯出的識別符號。為了讓我們的實現的更輕鬆,我們可以用點來匯入包 a:
import . "a"
然後引用包 a 中的匯出識別符號就不需要帶上包名(就像測試是在同一個包中一樣,但是那些非匯出的識別符號是不能訪問的)
如果匯入的包中存在至少一個同名的匯出識別符號,則無法使用點作為包名匯入兩個包:
// github.com/mlowicki/c package c var V = "c" // github.com/mlowkci/b package b var V = "b" // github.com/mlowicki/a package main import ( "fmt" . "github.com/mlowicki/b" . "github.com/mlowicki/c" ) func main() { fmt.Println(V) }
> go run main.go // command-line-arguments ./main.go:6:2: V redeclared during import "github.com/mlowicki/c" previous declaration during import "github.com/mlowicki/b" ./main.go:6:2: imported and not used: "github.com/mlowicki/c"
使用空識別符號
如果匯入了包但是不使用,Golang的編譯器將無法編譯通過。
package main import "fmt" func main() {}
使用點匯入,其中所有匯出的識別符號都直接新增到匯入檔案塊中,在編譯時也會出現失敗。唯一的繞過方式是使用空白識別符號。需要知道init函式是什麼,以便理解為什麼我們需要匯入空白識別符號。參考之前init的介紹文章https://medium.com/golangspec/init-functions-in-go-eac191b3860a 我鼓勵從上到下閱讀這篇文章,但本質上,像如下的匯入方式:
import _ "math"
不需要在匯入檔案中使用包 math,但是無論如何都將執行匯入包中的 init 函式(包和它的依賴關係將被初始化)。 如果我們只對匯入包完成的初始化工作感興趣,但我們不引用任何的匯出識別符號,那麼就很有用。
如果一個包被匯入沒有被使用或者沒有使用空識別符號,那將編譯失敗
迴圈匯入
Go 規範明確禁止迴圈匯入 - 當包間接匯入自身時。 最明顯的情況是包 a 匯入包 b 然後包 b 中也匯入包 a:
// github.com/mlowicki/a/main.go package a import "github.com/mlowicki/b" var A = b.B // github.com/mlowicki/b/main.go package b import "github.com/mlowicki/a" var B = a.A
嘗試構建這兩個包中的任何一個都會導致錯誤:
> go build can't load package: import cycle not allowed package github.com/mlowicki/a imports github.com/mlowicki/b imports github.com/mlowicki/a
當然,比如 a -> b -> c -> d -> a 這種情況更加的複雜(x -> y 指包 x 匯入包 y)。
包也是不能匯入自己的:
package main import ( "fmt" "github.com/mlowicki/a" ) var A = "a" func main() { fmt.Println(a.A) }
編譯上述程式碼將會提示錯誤:can’t load package: import cycle not allowed 。
(完)