理解Golang包匯入

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

轉自:http://tonybai.com/2015/03/09/understanding-import-packages/

Golang使用包(package)這種文法元素來組織源碼,所有文法可見度均定義在package這個層級,與Java 、python等語言相比,這算不上什麼創新,但與C傳統的include相比,則是顯得“先進”了許多。

Golang中包的定義和使用看起來十分簡單:

通過package關鍵字定義包:

   package xxx

使用import關鍵字,匯入要使用的標準庫包或第三方依賴包。

   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做首碼包名:

   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
   
一、編譯時間使用的是包源碼還是.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.gopackage fooimport "fmt"func Foo1() {    fmt.Println("Foo1")}
// main.gopackage mainimport (    "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.gopackage fooimport "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.goimport "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源碼。

二、目錄名還是包名?

從第一節的實驗中,我們得知了編譯器在編譯過程中依賴的是包源碼的路徑,這為後續的實驗打下了基礎。下面我們再來看看,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.gopackage barimport "fmt"func Bar1() {    fmt.Println("Bar1")}

注意:這裡package名為bar,與目錄名foo完全不同。

接下來就給app2帶來了難題:該如何import bar包呢?

我們假設import路徑中的最後一個元素是包名,而非路徑名。

//app2/main.gopackage 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)

編譯失敗,在兩個路徑下無法找到對應libproj2/bar包。

我們的假設錯了,我們把它改為路徑:

//app2/main.gopackage mainimport (    "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包的源檔案。

三、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"該如何理解呢? 

根據上面一、二兩節中得出的結論,我們嘗試理解一下m:(4)m指代的是lib/math路徑下唯一的那個包

一個目錄下是否可以存在兩個包呢?我們來試試。

我們在libproj1/foo下新增一個go源檔案,bar1.go:

package barimport "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.gopackage mainimport 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.go
6l – $GOROOT/src/cmd/ld/doc.go

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.