Golang import 包問題相關詳解

1 本地包聲明

包是Go程序的基本單位,所以每個Go程序源代碼的開始都是一個包聲明:

package pkgName
  • 1

這就是包聲明,pkgName 告訴編譯器,當前文件屬於哪個包。一個包可以對應多個*.go源文件,標記它們屬於同一包的唯一依據就是這個package聲明,也就是說:無論多少個源文件,只要它們開頭的package包相同,那麼它們就屬於同一個包,在編譯後就只會生成一個.a文件,並且存放在$GOPATH/pkg文件夾下。

示例:

(1) 我們在$GOPATH/目錄下,創建如下結構的文件夾和文件:
這裏寫圖片描述

分別寫入如下的代碼:

hello.go

//hello.go
package hello

import (
    "fmt"
)

func SayHello() {
    fmt.Println("SayHello()-->Hello")
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

hello2.go

//hello2.go
package hello

import (
    "fmt"
)

func SayWorld() {
    fmt.Println("SayWorld()-->World")
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

main.go

//main.go
package main

import (
    "hello"
)

func main() {
    hello.SayHello()
    hello.SayWorld()
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

分析:

根據hello.go/hello2.go中的package聲明可知,它們倆屬於同一個包–hello,那麼根據分析,編譯後只會生成一個*.a文件。

執行命令:

go install hello
  • 1

該命令的意思大概是:編譯並安裝hello包,這裏安裝的意思是將生成的*.a文件放到工作目錄$GOPATH/pkg目錄下去

運行後:

這裏寫圖片描述

從結果看出,果然只生成了一個包,並且名爲hello.a

那麼我們提出第二個問題:生成的*.a文件名是否就是我們定義的包名+.a後綴?

爲了驗證這個問題,我們對源碼做一些更改:
將hello.go/hello2.go中的package聲明改爲如下:

package hello_a
  • 1

在編譯安裝包之前,先清除上一次生成的包:

go clean -i hello
  • 1

再次編譯安裝該包:

go install hello_a
  • 1

按照“正常推理”,上面這句命令是沒什麼問題的,因爲我們已經將包名改成hello_a了啊,但是實際的運行結果是這樣的:

這裏寫圖片描述

oh~No!!
那麼,我們再試試用這條命令:

go install hello
  • 1

臥槽!!居然成功了!!是不是??

這裏寫圖片描述

那麼我們嘗試生成一下可執行程序,看看能不能正常運行呢?

go build main
  • 1

又報錯了!!!

這裏寫圖片描述

看這報錯提示,好像應該改一下main.go源碼,那就改成如下吧:

//main.go
package main

import (
    "hello_a"
)

func main() {
    hello_a.SayHello()
    hello_a.SayWorld()
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

改成上面這樣也合情合理哈?畢竟我們把包名定義成了hello_a了!
那就再來編譯一次吧:

go build main
  • 1

繼續報錯!
這裏寫圖片描述

等等!!有新的發現,對比上兩次的報錯信息,可見第一次還能能找到hello_a包的,更改源碼後居然還TM找不到hello_a包了??
好吧,那咱再改回去,不過這回只改包的導入語句,改成:

import (
    "hello"
)
  • 1
  • 2
  • 3

再次編譯:

go build main
  • 1

臥槽!!居然沒報錯了!!再運行一下可執行程序:

這裏寫圖片描述

好吧,終於得到了想要的結果!

那進行到這裏能說明什麼呢?

(1) 一個包確實可以由多個源文件組成,只要它們開頭的包聲明一樣

(2)一個包對應生成一個*.a文件,生成的文件名並不是包名+.a
(3) go install ××× 這裏對應的並不是包名,而是路徑名!!
(4) import ××× 這裏使用的也不是包名,也是路徑名!
(5) ×××××.SayHello() 這裏使用的纔是包名!

那麼問題又來了,我們該如何理解(3)、(4)中的路徑名呢?
我覺得,可以這樣理解:
這裏指定的是該×××路徑名就代表了此目錄下唯一的包,編譯器連接器默認就會去生成或者使用它,而不需要我們手動指明!

好吧,問題又來了,如果一個目錄下有多個包可以嗎?如果可以,那該怎麼編譯和使用??

那我們繼續改改源代碼:
首先,保持hello2.go 不變,改動hello.go爲如下代碼:

//hello.go
package hello

import (
    "fmt"
)

func SayHello() {
    fmt.Println("SayHello()-->Hello")
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

並且更改main.go的源碼如下

//main.go
package main

import (
    "hello"
)

func main() {
    hello.SayHello()
    hello_a.SayWorld()
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

再次清理掉上次生成的可執行程序與包:

go clean -i hello
go clean -x main
  • 1
  • 2

你可以試着執行如上的命令,如果不能清除,那就手動刪除吧!
反正,還原成如下樣子:

這裏寫圖片描述

那麼再次嘗試編譯並安裝包,不過注意了,此時hello目錄下有兩個包了,不管是否正確,我們先嚐試一下:

go install hello
  • 1

oh~~果然出錯了!!
這裏寫圖片描述
看到了嗎?它說它找到了兩個包了啊!!!

那這能說明什麼呢??

其實這就更加確定的說明了,我們上面的推測是正確的!

(3) go install ××× 這裏對應的並不是包名,而是路徑名!!

這裏指定的是該×××路徑名就代表了此目錄下唯一的包,編譯器連接器默認就會去生成或者使用它,而不需要我們手動指明!

好吧,證明了這個還是挺興奮的!!那我們繼續!!

如果一個目錄下,真的有兩個或者更多個包,那該如何生成??
抱着試一試的態度,我嘗試了許多可能,但無一正確,最後一個命令的結果是讓我崩潰的:

go help install
  • 1

這裏寫圖片描述

恩!對!你沒有看錯:installs the packages named by the import paths
What the fuck!! 以後還是決定要先看文檔再自己做測試!!

好吧,綜上所述,一個目錄下就只能有一個包吧,因爲都是指定路徑,沒有辦法指定路徑下的某個具體的包,這樣的做法其實也挺好,讓源代碼結構更清晰!

 
 
2 包的導入問題

首先,還是對上面的示例程序做一個更改,這次我們讓它變得更加簡單點,因爲接下來討論的東西,可能會稍微有點繞~~

首先,刪除hello2.go,清理掉編譯生成的文件,其他文件內容如下:
hello.go

//hello.go
package hello

import (
    "fmt"
)

func SayHello() {
    fmt.Println("SayHello()-->Hello")
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

main.go

//main.go
package main

import (
    "hello"
)

func main() {
    hello.SayHello()
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

最後,讓整體保持如下的樣式:
這裏寫圖片描述

我們先編譯一次,讓程序能夠運行起來:

go install hello
go build main
./main
  • 1
  • 2
  • 3

好吧,假如你能看到輸出,那就沒問題了!
此時,再來看看整體的結構:
這裏寫圖片描述

按照C/C++的方式來說,此時生成了hello.a這個鏈接庫,那麼源文件那些應該就沒有必要了吧,所以。。。。我們這樣搞一下,我們來更改一下hello.go源碼,但不編譯它!
hello.go

//hello.go
package hello

import (
    "fmt"
)

func SayHello() {
    fmt.Println("SayHello()-->Hello_modifi...")
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

然後,我們刪除之前的可執行文件main,再重新生成它:

rm main
go build main
  • 1
  • 2

恩~~等等,我看一下運行結果:

這裏寫圖片描述

What the fuck!!!爲什麼出來的是這貨???

好吧,爲了一探究竟,我們再次刪除main文件,並再次重新編譯,不過命令上得做點手腳,我們要看看編譯器連接器這兩個小婊砸到底都幹了些什麼,爲啥是隔壁老王的兒子出來了??!!

rm main
go build -x -v main
  • 1
  • 2

結果:

這裏寫圖片描述

那麼我們一步一步對這個結果做一個分析:

#首先,它好像指定了一個臨時工作目錄
WORK=/tmp/go-build658882358 

#看着樣子,它好像是要準備編譯hello目錄下的包
hello
#然後創建了一系列臨時文件夾
mkdir -p $WORK/hello/_obj/    
mkdir -p $WORK/

#進入包的源文件目錄
cd /home/yuxuan/GoProjects/import/src/hello 

#調用6g這個編譯器編譯生成hello.a,存放在$WORK/臨時目錄下
/opt/go/pkg/tool/linux_amd64/6g -o $WORK</span>/hello.a -trimpath <span class="hljs-variable">$WORK -p hello -complete -D _/home/yuxuan/GoProjects/import/src/hello -I $WORK -pack ./hello.go

#要編譯main目錄下的包了
main
#還是創建一系列的臨時文件夾
mkdir -p $WORK/main/_obj/   
mkdir -p $WORK/main/_obj/exe/

#進入main文件夾
cd /home/yuxuan/GoProjects/import/src/main

#調用6g編譯器,編譯生成main.a,存放於$WORK/臨時目錄下
/opt/go/pkg/tool/linux_amd64/6g -o $WORK</span>/main.a -trimpath <span class="hljs-variable">$WORK -p main -complete -D _/home/yuxuan/GoProjects/import/src/main -I $WORK -I /home/yuxuan/GoProjects/import/pkg/linux_amd64 -pack ./main.go

#最後它進入了一個“當前目錄”,應該就是我們執行go build命令的目錄
cd .

#調用連接器6l 然後它鏈接生成a.out,存放與臨時目錄下的$WORK/main/_obj/exe/文件夾中,但是在鏈接選項中並未直接發現hello.a
#從鏈接選項:-L $WORK -L /home/yuxuan/GoProjects/import/pkg/linux_amd64中可以看出,連接器首先搜索了$WORK臨時目錄下的所有*.a文件,然後再去搜索/home/yuxuan/GoProjects/import/pkg/linux_amd64目錄下的*.a文件,可見原因
/opt/go/pkg/tool/linux_amd64/6l -o $WORK</span>/main/_obj/exe/a.out -L <span class="hljs-variable">$WORK -L /home/yuxuan/GoProjects/import/pkg/linux_amd64 -extld=gcc $WORK/main.a

#最後,移動可執行文件並重命名
mv $WORK/main/_obj/exe/a.out main
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

到這裏,其實差不多也就得出結論了,連接器在連接時,其實使用的並不是我們工作目錄下的hello.a文件,而是以該最新源碼編譯出的臨時文件夾中的hello.a文件。

當然,如果你對這個結論有所懷疑,可以試試手動執行上述命令,在最後鏈接時,去掉-L $WORK的選項,再看看運行結果!

那麼,這是對於有源代碼的第三方庫,如果沒有源代碼呢?

其實,結果顯而易見,沒有源代碼,上面的臨時編譯不可能成功,那麼臨時目錄下就不可能有.a文件,所以最後鏈接時就只能鏈接到工作目錄下的.a文件!

但是,如果是自帶的Go標準庫呢?

其實也可以用上述的方法驗證一下,驗證過程就不寫了吧?
最後得到的結果是:對於標準庫,即便是修改了源代碼,只要不重新編譯Go源碼,那麼鏈接時使用的就還是已經編譯好的*.a文件!

3 導入包的三種模式

包導入有三種模式:正常模式、別名模式、簡便模式

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肯定是包名了!
那問題又來了,import m “lib/math”該如何理解呢?

根據上面得出的結論,我們嘗試這樣理解m:m指代的是lib/math路徑下唯一的那個包!

 
 
4 總結

經過上面這一長篇大論,是時候該總結一下成果了:

  • 多個源文件可同屬於一個包,只要聲明時package指定的包名一樣;
  • 一個包對應生成一個*.a文件,生成的文件名並不是包名+.a組成,應該是目錄名+.a組成
  • go install ××× 這裏對應的並不是包名,而是路徑名!!
  • import ××× 這裏使用的也不是包名,也是路徑名
  • ×××××.SayHello() 這裏使用的纔是包名!
  • 指定×××路徑名就代表了此目錄下唯一的包,編譯器連接器默認就會去生成或者使用它,而不需要我們手動指明!
  • 一個目錄下就只能有一個包存在
  • 對於調用有源碼的第三方包,連接器在連接時,其實使用的並不是我們工作目錄下的.a文件,而是以該最新源碼編譯出的臨時文件夾中的.a文件
  • 對於調用沒有源碼的第三方包,上面的臨時編譯不可能成功,那麼臨時目錄下就不可能有.a文件,所以最後鏈接時就只能鏈接到工作目錄下的.a文件
  • 對於標準庫,即便是修改了源代碼,只要不重新編譯Go源碼,那麼鏈接時使用的就還是已經編譯好的*.a文件
  • 包導入有三種模式:正常模式、別名模式、簡便模式
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章