轉自:http://tonybai.com/2015/03/09/understanding-import-packages/python
Golang使用包(package)這種語法元素來組織源碼,全部語法可見性均定義在package這個級別,與Java 、python等語言相比,這算不上什麼創新,但與C傳統的include相比,則是顯得「先進」了許多。git
Golang中包的定義和使用看起來十分簡單:github
經過package關鍵字定義包:golang
package xxxapi
使用import關鍵字,導入要使用的標準庫包或第三方依賴包。app
import "a/b/c" import "fmt" c.Func1() fmt.Println("Hello, World")
不少Golang初學者看到上面代碼,都會想固然的將import後面的"c"、"fmt"當成包名,將其與c.Func1()和 fmt.Println()中的c和fmt認做爲同一個語法元素:包名。但在深刻Golang後,不少人便會發現事實上並不是如此。好比在使用實時分佈式消 息平臺nsq提供的go client api時:分佈式
咱們導入的路徑以下:函數
import 「github.com/bitly/go-nsq」測試
但在使用其提供的export functions時,卻用nsq作前綴包名:ui
q, _ := nsq.NewConsumer("write_test", "ch", config)
人們不由要問:import後面路徑中的最後一個元素到底表明的是啥? 是包名仍是僅僅是一個路徑?咱們一塊兒經過試驗來理解一下。 實驗環境:darwin_amd64 , go 1.4。
初始試驗環境目錄結果以下:
GOPATH = /Users/tony/Test/Go/pkgtest/
pkgtest/
pkg/
src/
libproj1/
foo/
foo1.go
app1/
main.go
1、編譯時使用的是包源碼仍是.a
咱們知道一個非main包在編譯後會生成一個.a文件(在臨時目錄下生成,除非使用go install安裝到$GOROOT或$GOPATH下,不然你看不到.a),用於後續可執行程序連接使用。
好比Go標準庫中的包對應的源碼部分路徑在:$GOROOT/src,而標準庫中包編譯後的.a文件路徑在$GOROOT/pkg/darwin_amd64下。一個奇怪的問題在我腦殼中升騰起來,編譯時,編譯器到底用的是.a仍是源碼?
咱們先以用戶自定義的package爲例作個小實驗。
$GOPATH/src/
libproj1/foo/
– foo1.go
app1
– main.go
//foo1.go package foo import "fmt" func Foo1() { fmt.Println("Foo1") }
// main.go package main import ( "libproj1/foo" ) func main() { foo.Foo1() }
執行go install libproj1/foo,Go編譯器編譯foo包,並將foo.a安裝到$GOPATH/pkg/darwin_amd64/libproj1下。
編譯app1:go build app1,在app1目錄下生成app1*可執行文件,執行app1,咱們獲得一個初始預期結果:
$./app1
Foo1
如今咱們沒法看出使用的究竟是foo的源碼仍是foo.a,由於目前它們的輸出都是一致的。咱們修改一下foo1.go的代碼:
//foo1.go package foo import "fmt" func Foo1() { fmt.Println("Foo1 – modified") }
從新編譯執行app1,咱們獲得結果以下:
$./app1
Foo1 – modified
實際測試結果告訴咱們:(1)在使用第三方包的時候,當源碼和.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包管理的一個特色。
其實經過編譯器的詳細輸出咱們也可得出上面結論。咱們在編譯app1時給編譯器傳入-x -v選項:
$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連接器的執行語句中並未顯式看到app1連接的是$WORK/libproj1下的foo.a。可是從6l連接器的-L參數來看:-L $WORK -L /Users/tony/Test/Go/pkgtest/pkg/darwin_amd64,咱們發現$WORK目錄放在了前面,咱們猜想6l首先搜索到的時$WORK下面的libproj1/foo.a。
爲了驗證咱們的推論,咱們按照編譯器輸出,按順序手動執行了一遍如上命令,但在最後執行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
這樣作的結果是:
$./app1
Foo1
編譯器連接了$GOPATH/pkg下的foo.a。(2)到這裏咱們明白了所謂的使用第三方包源碼,其實是連接了以該最新源碼編譯的臨時目錄下的.a文件而已。
Go標準庫中的包也是這樣麼?對於標準庫,好比fmt而言,編譯時,到底使用的時$GOROOT/src下源碼仍是$GOROOT/pkg下已經編譯好的.a呢?
咱們不妨也來試試,一個最簡單的hello world例子:
//main.go import "fmt" func main() { fmt.Println("Hello, World") }
咱們先將$GOROOT/src/fmt目錄rename 爲fmtbak,看看go compiler有何反應?
go build -x -v ./
$go build -x -v ./
WORK=/var/folders/2h/xr2tmnxx6qxc4w4w13m01fsh0000gn/T/go-build957202426
main.go:4:8: cannot find package "fmt" in any of:
/Users/tony/.Bin/go14/src/fmt (from $GOROOT)
/Users/tony/Test/Go/pkgtest/src/fmt (from $GOPATH)
找不到fmt包了。顯然標準庫在編譯時也是必需要源碼的。不過與自定義包不一樣的是,即使你修改了fmt包的源碼(未從新編譯GO安裝包),用戶源碼編譯時,也不會嘗試從新編譯fmt包的,依舊只是在連接時連接已經編譯好的fmt.a。經過下面的gc輸出能夠驗證這點:
$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
能夠看出,編譯器的確並何嘗試編譯標準庫中的fmt源碼。
2、目錄名仍是包名?
從第一節的實驗中,咱們得知了編譯器在編譯過程當中依賴的是包源碼的路徑,這爲後續的實驗打下了基礎。下面咱們再來看看,Go語言中import後面路徑中最後的一個元素究竟是包名仍是路徑名?
本次實驗目錄結構:
$GOPATH
src/
libproj2/
foo/
foo1.go
app2/
main.go
按照Golang語言習慣,一個go package的全部源文件放在同一個目錄下,且該目錄名與該包名相同,好比libproj1/foo目錄下的package爲foo,foo1.go、 foo2.go…共同組成foo package的源文件。但目錄名與包名也能夠不一樣,咱們就來試試不一樣的。
咱們創建libproj2/foo目錄,其中的foo1.go代碼以下:
//foo1.go package bar import "fmt" func Bar1() { fmt.Println("Bar1") }
注意:這裏package名爲bar,與目錄名foo徹底不一樣。
接下來就給app2帶來了難題:該如何import bar包呢?
咱們假設import路徑中的最後一個元素是包名,而非路徑名。
//app2/main.go package main import ( "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)
編譯失敗,在兩個路徑下沒法找到對應libproj2/bar包。
咱們的假設錯了,咱們把它改成路徑:
//app2/main.go package main import ( "libproj2/foo" ) func main() { bar.Bar1() }
再編譯執行:
$go build app2
$app2
Bar1
這回編譯順利經過,執行結果也是OK的。這樣咱們獲得告終論:(3)import後面的最後一個元素應該是路徑,就是目錄,並不是包名。
go編譯器在這些路徑(libproj2/foo)下找bar包。這樣看來,go語言的慣例只是一個特例,即剛好目錄名與包名一致罷了。也就是說下面例子中的兩個foo含義不一樣:
import "libproj1/foo" func main() { foo.Foo() }
import中的foo只是一個文件系統的路徑罷了。而下面foo.Foo()中的foo則是包名。而這個包是在libproj1/foo目錄下的源碼中找到的。
再類比一下標準庫包fmt。
import "fmt"
fmt.Println("xxx")
這裏上下兩行中雖然都是「fmt",但一樣含義不一樣,一個是路徑 ,對於標準庫來講,是$GOROOT/src/fmt這個路徑。而第二行中的fmt則是包名。gc會在$GOROOT/src/fmt路徑下找到fmt包的源文件。
3、import m "lib/math"
Go language specification中關於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包中的導出函數Sin。
那m究竟是包名仍是路徑呢?既然能經過m訪問Sin,那m確定是包名了,Right!那import m "lib/math"該如何理解呢?
根據上面1、二兩節中得出的結論,咱們嘗試理解一下m:(4)m指代的是lib/math路徑下惟一的那個包。
一個目錄下是否能夠存在兩個包呢?咱們來試試。
咱們在libproj1/foo下新增一個go源文件,bar1.go:
package bar import "fmt" func Bar1() { fmt.Println("Bar1") }
咱們從新構建一下這個目錄下的包:
$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
咱們收到了錯誤提示,編譯器在這個路徑下發現了兩個包,這是不容許的。
咱們再做個實驗,來驗證咱們對m含義的解釋。
咱們創建app3目錄,其main.go的源碼以下:
//main.go package main import m "libproj2/foo" func main() { m.Bar1() }
libproj2/foo路徑下的包的包名爲bar,按照咱們的推論,m指代的就是bar這個包,經過m咱們能夠訪問bar的Bar1導出函數。
編譯並執行上面main.go:
$go build app3
$app3
Bar1
執行結果與咱們推論徹底一致。
附錄:6g, 6l文檔位置:
6g – $GOROOT/src/cmd/gc/doc.go6l – $GOROOT/src/cmd/ld/doc.go