深入理解Golang包导入
Golang使用包(package)這種語法元素來組織源碼,所有語法可見性均定義在package這個級別,與Java 、python等語言相比,這算不上什么創(chuàng)新,但與C傳統(tǒng)的include相比,則是顯得“先進(jìn)”了許多。
Golang中包的定義和使用看起來十分簡單:
通過package關(guān)鍵字定義包:
package xxx使用import關(guān)鍵字,導(dǎo)入要使用的標(biāo)準(zhǔn)庫包或第三方依賴包。??
import "a/b/c"import "fmt"c.Func1()fmt.Println("Hello, World")很多Golang初學(xué)者看到上面代碼,都會想當(dāng)然的將import后面的"c"、"fmt"當(dāng)成包名,將其與c.Func1()和 fmt.Println()中的c和fmt認(rèn)作為同一個語法元素:包名。但在深入Golang后,很多人便會發(fā)現(xiàn)事實(shí)上并非如此。比如在使用實(shí)時分布式消息平臺nsq提供的go client api時:
我們導(dǎo)入的路徑如下:
import “github.com/bitly/go-nsq”但在使用其提供的export functions時,卻用nsq做前綴包名:
?? q, _ := nsq.NewConsumer("write_test", "ch", config)
人們不禁要問:import后面路徑中的最后一個元素到底代表的是啥? 是包名還是僅僅是一個路徑?我們一起通過試驗(yàn)來理解一下。? 實(shí)驗(yàn)環(huán)境:darwin_amd64 , go 1.4。
初始試驗(yàn)環(huán)境目錄結(jié)構(gòu)如下:
GOPATH = /Users/tony/Test/Go/pkgtest/
pkgtest/
??? pkg/
??? src/
? ???? libproj1/
??? ?????? foo/
??? ? ? ? ??? foo1.go
??? ?? app1/
? ??? ???? main.go
???
一、編譯時使用的是包源碼還是.a
我們知道一個非main包在編譯后會生成一個.a文件(在臨時目錄下生成,除非使用go install安裝到$GOROOT或$GOPATH下,否則你看不到.a),用于后續(xù)可執(zhí)行程序鏈接使用。
比如Go標(biāo)準(zhǔn)庫中的包對應(yīng)的源碼部分路徑在:$GOROOT/src,而標(biāo)準(zhǔn)庫中包編譯后的.a文件路徑在$GOROOT/pkg/darwin_amd64下。一個奇怪的問題在我腦袋中升騰起來,編譯時,編譯器到底用的是.a還是源碼?
我們先以用戶自定義的package為例做個小實(shí)驗(yàn)。
$GOPATH/src/
??? libproj1/foo/
??? ??? ??? – foo1.go
??? app1/
??? ??? ??? – main.go
//foo1.go
package fooimport "fmt"func Foo1() {fmt.Println("Foo1") }// main.go
package mainimport ("libproj1/foo" )func main() {foo.Foo1() }執(zhí)行go install libproj1/foo,Go編譯器編譯foo包,并將foo.a安裝到$GOPATH/pkg/darwin_amd64/libproj1下。
編譯app1:go build app1,在app1目錄下生成app1*可執(zhí)行文件,執(zhí)行app1,我們得到一個初始預(yù)期結(jié)果:
現(xiàn)在我們無法看出使用的到底是foo的源碼還是foo.a,因?yàn)槟壳八鼈兊妮敵龆际且恢碌?。我們修改一下foo1.go的代碼://foo1.go
package fooimport "fmt"func Foo1() {fmt.Println("Foo1 – modified") }重新編譯執(zhí)行app1,我們得到結(jié)果如下:
$./app1 Foo1 – modified實(shí)際測試結(jié)果告訴我們:(1)在使用第三方包的時候,當(dāng)源碼和.a均已安裝的情況下,編譯器鏈接的是源碼。
那么是否可以只鏈接.a,不用第三方包源碼呢?我們臨時刪除掉libproj1目錄,但保留之前install的libproj1/foo.a文件。
我們再次嘗試編譯app1,得到如下錯誤:
$go build app1 main.go:5:2: cannot find package "libproj1/foo" in any of:/Users/tony/.Bin/go14/src/libproj1/foo (from $GOROOT)/Users/tony/Test/Go/pkgtest/src/libproj1/foo (from $GOPATH)編譯器還是去找源碼,而不是.a,因此我們要依賴第三方包,就必須搞到第三方包的源碼,這也是Golang包管理的一個特點(diǎn)。
其實(shí)通過編譯器的詳細(xì)輸出我們也可得出上面結(jié)論。我們在編譯app1時給編譯器傳入-x -v選項(xiàng):
$go build -x -v app1 WORK=/var/folders/2h/xr2tmnxx6qxc4w4w13m01fsh0000gn/T/go-build797811168 libproj1/foo mkdir -p $WORK/libproj1/foo/_obj/ mkdir -p $WORK/libproj1/ cd /Users/tony/Test/Go/pkgtest/src/libproj1/foo /Users/tony/.Bin/go14/pkg/tool/darwin_amd64/6g -o $WORK/libproj1/foo.a -trimpath $WORK -p libproj1/foo -complete -D _/Users/tony/Test/Go/pkgtest/src/libproj1/foo -I $WORK -pack ./foo1.go ./foo2.go app1 mkdir -p $WORK/app1/_obj/ mkdir -p $WORK/app1/_obj/exe/ cd /Users/tony/Test/Go/pkgtest/src/app1 /Users/tony/.Bin/go14/pkg/tool/darwin_amd64/6g -o $WORK/app1.a -trimpath $WORK -p app1 -complete -D _/Users/tony/Test/Go/pkgtest/src/app1 -I $WORK -I /Users/tony/Test/Go/pkgtest/pkg/darwin_amd64 -pack ./main.go cd . /Users/tony/.Bin/go14/pkg/tool/darwin_amd64/6l -o $WORK/app1/_obj/exe/a.out -L $WORK -L /Users/tony/Test/Go/pkgtest/pkg/darwin_amd64 -extld=clang $WORK/app1.a mv $WORK/app1/_obj/exe/a.out app1可以看到編譯器6g首先在臨時路徑下編譯出依賴包foo.a,放在$WORK/libproj1下。但我們在最后6l鏈接器的執(zhí)行語句中并未顯式看到app1鏈接的是$WORK/libproj1下的foo.a。但是從6l鏈接器的-L參數(shù)來看:-L $WORK -L /Users/tony/Test/Go/pkgtest/pkg/darwin_amd64,我們發(fā)現(xiàn)$WORK目錄放在了前面,我們猜測6l首先搜索到的是$WORK下面的libproj1/foo.a。
為了驗(yàn)證我們的推論,我們按照編譯器輸出,按順序手動執(zhí)行了一遍如上命令,但在最后執(zhí)行6l命令時,去掉了-L $WORK:
/Users/tony/.Bin/go14/pkg/tool/darwin_amd64/6l -o $WORK/app1/_obj/exe/a.out -L /Users/tony/Test/Go/pkgtest/pkg/darwin_amd64 -extld=clang $WORK/app1.a
這樣做的結(jié)果是:
$./app1 Foo1編譯器鏈接了$GOPATH/pkg下的foo.a。(2)到這里我們明白了所謂的使用第三方包源碼,實(shí)際上是鏈接了以該最新源碼編譯的臨時目錄下的.a文件而已。
Go標(biāo)準(zhǔn)庫中的包也是這樣么?對于標(biāo)準(zhǔn)庫,比如fmt而言,編譯時,到底使用的是$GOROOT/src下源碼還是$GOROOT/pkg下已經(jīng)編譯好的.a呢?我們不妨也來試試,一個最簡單的hello world例子:
//main.go
我們先將$GOROOT/src/fmt目錄rename 為fmtbak,看看go compiler有何反應(yīng)?
go build -x -v ./
找不到fmt包了。顯然標(biāo)準(zhǔn)庫在編譯時也是必須要源碼的。不過與自定義包不同的是,即便你修改了fmt包的源碼(未重新編譯GO安裝包),用戶源碼編譯時,也不會嘗試重新編譯fmt包的,依舊只是在鏈接時鏈接已經(jīng)編譯好的fmt.a。通過下面的gc輸出可以驗(yàn)證這點(diǎn):
$go build -x -v ./ WORK=/var/folders/2h/xr2tmnxx6qxc4w4w13m01fsh0000gn/T/go-build773440756 app1 mkdir -p $WORK/app1/_obj/ mkdir -p $WORK/app1/_obj/exe/ cd /Users/tony/Test/Go/pkgtest/src/app1 /Users/tony/.Bin/go14/pkg/tool/darwin_amd64/6g -o $WORK/app1.a -trimpath $WORK -p app1 -complete -D _/Users/tony/Test/Go/pkgtest/src/app1 -I $WORK -pack ./main.go cd . /Users/tony/.Bin/go14/pkg/tool/darwin_amd64/6l -o $WORK/app1/_obj/exe/a.out -L $WORK -extld=clang $WORK/app1.a mv $WORK/app1/_obj/exe/a.out app1可以看出,編譯器的確并未嘗試編譯標(biāo)準(zhǔn)庫中的fmt源碼。
?
二、目錄名還是包名?
從第一節(jié)的實(shí)驗(yàn)中,我們得知了編譯器在編譯過程中依賴的是包源碼的路徑,這為后續(xù)的實(shí)驗(yàn)打下了基礎(chǔ)。下面我們再來看看,Go語言中import后面路徑中最后的一個元素到底是包名還是路徑名?
本次實(shí)驗(yàn)?zāi)夸浗Y(jié)構(gòu):
$GOPATH
??? src/
??? ?? libproj2/
???? ??? ??? foo/
??? ?? ??? ??? foo1.go
?????? app2/
??????? ???? main.go
按照Golang語言習(xí)慣,一個go package的所有源文件放在同一個目錄下,且該目錄名與該包名相同,比如libproj1/foo目錄下的package為foo,foo1.go、 foo2.go…共同組成foo package的源文件。但目錄名與包名也可以不同,我們就來試試不同的。
我們建立libproj2/foo目錄,其中的foo1.go代碼如下://foo1.go
package barimport "fmt"func Bar1() {fmt.Println("Bar1") }注意:這里package名為bar,與目錄名foo完全不同。
接下來就給app2帶來了難題:該如何import bar包呢?
我們假設(shè)import路徑中的最后一個元素是包名,而非路徑名。
//app2/main.go
package mainimport ("libproj2/bar" )func main() {bar.Bar1() }編譯app2:
$go build -x -v app2 WORK=/var/folders/2h/xr2tmnxx6qxc4w4w13m01fsh0000gn/T/go-build736904327 main.go:5:2: cannot find package "libproj2/bar" in any of:/Users/tony/.Bin/go14/src/libproj2/bar (from $GOROOT)/Users/tony/Test/Go/pkgtest/src/libproj2/bar (from $GOPATH)編譯失敗,在兩個路徑下無法找到對應(yīng)libproj2/bar包。
我們的假設(shè)錯了,我們把它改為路徑:
//app2/main.go
package mainimport ("libproj2/foo" )func main() {bar.Bar1() }再編譯執(zhí)行:
$go build app2 $app2 Bar1這回編譯順利通過,執(zhí)行結(jié)果也是OK的。這樣我們得到了結(jié)論:(3)import后面的最后一個元素應(yīng)該是路徑,就是目錄,并非包名。
go編譯器在這些路徑(libproj2/foo)下找bar包。這樣看來,go語言的慣例只是一個特例,即恰好目錄名與包名一致罷了。也就是說下面例子中的兩個foo含義不同:
import "libproj1/foo"func main() {foo.Foo() }import中的foo只是一個文件系統(tǒng)的路徑罷了。而下面foo.Foo()中的foo則是包名。而這個包是在libproj1/foo目錄下的源碼中找到的。
再類比一下標(biāo)準(zhǔn)庫包fmt。
import "fmt" fmt.Println("xxx")這里上下兩行中雖然都是“fmt",但同樣含義不同,一個是路徑 ,對于標(biāo)準(zhǔn)庫來說,是$GOROOT/src/fmt這個路徑。而第二行中的fmt則是包名。gc會在$GOROOT/src/fmt路徑下找到fmt包的源文件。
?
三、import m "lib/math"
Go language specification中關(guān)于import package時列舉的一個例子如下:
Import declaration????????? Local name of Sin
import?? "lib/math"???????? math.Sin
import m "lib/math"???????? m.Sin
import . "lib/math"???????? Sin
我們看到import m "lib/math"? m.Sin一行。我們說過lib/math是路徑,import語句用m替代lib/math,并在代碼中通過m訪問math包中的導(dǎo)出函數(shù)Sin。
那m到底是包名還是路徑呢?既然能通過m訪問Sin,那m肯定是包名了,Right!那import m "lib/math"該如何理解呢??
根據(jù)上面一、二兩節(jié)中得出的結(jié)論,我們嘗試?yán)斫庖幌耺:(4)m指代的是lib/math路徑下唯一的那個包。
一個目錄下是否可以存在兩個包呢?我們來試試。
我們在libproj1/foo下新增一個go源文件,bar1.go:
package barimport "fmt"func Bar1() {fmt.Println("Bar1") }我們重新構(gòu)建一下這個目錄下的包:
$go build libproj1/foo can't load package: package libproj1/foo: found packages bar1.go (bar) and foo1.go (foo) in /Users/tony/Test/Go/pkgtest/src/libproj1/foo我們收到了錯誤提示,編譯器在這個路徑下發(fā)現(xiàn)了兩個包,這是不允許的。
我們再作個實(shí)驗(yàn),來驗(yàn)證我們對m含義的解釋。
我們建立app3目錄,其main.go的源碼如下://main.go
package mainimport m "libproj2/foo"func main() {m.Bar1() }libproj2/foo路徑下的包的包名為bar,按照我們的推論,m指代的就是bar這個包,通過m我們可以訪問bar的Bar1導(dǎo)出函數(shù)。
編譯并執(zhí)行上面main.go:
$go build app3 $app3 Bar1執(zhí)行結(jié)果與我們推論完全一致。
附錄:6g, 6l文檔位置:
6g – $GOROOT/src/cmd/gc/doc.go
6l – $GOROOT/src/cmd/ld/doc.go
? 2015,?bigwhite. 版權(quán)所有.
?
Related posts:
總結(jié)
以上是生活随笔為你收集整理的深入理解Golang包导入的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 面试还在被红-黑树虐?看完这篇动图文章轻
- 下一篇: 深入浅出 Java 中 JVM 内存管理