1. 程式人生 > 程式設計 >Golang import本地包和匯入問題相關詳解

Golang import本地包和匯入問題相關詳解

1 本地包宣告

包是Go程式的基本單位,所以每個Go程式原始碼的開始都是一個包宣告:

package pkgName

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

示例:

(1) 我們在$GOPATH/目錄下,建立如下結構的資料夾和檔案:

Golang import本地包和匯入問題相關詳解

分別寫入如下的程式碼:

hello.go

//hello.go
package hello

import (
  "fmt"
)

func SayHello() {
  fmt.Println("SayHello()-->Hello")
}

hello2.go

//hello2.go
package hello

import (
  "fmt"
)

func SayWorld() {
  fmt.Println("SayWorld()-->World")
}

main.go

//main.go
package main

import (
  "hello"
)

func main() {
  hello.SayHello()
  hello.SayWorld()
}

分析:

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

執行命令:

go install hello

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

執行後:

Golang import本地包和匯入問題相關詳解

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

那麼我們提出第二個問題:生成的*.a檔名是否就是我們定義的包名+.a字尾?

為了驗證這個問題,我們對原始碼做一些更改:
將hello.go/hello2.go中的package宣告改為如下:

package hello_a

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

go clean -i hello

再次編譯安裝該包:

go install hello_a

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

Golang import本地包和匯入問題相關詳解

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

go install hello

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

Golang import本地包和匯入問題相關詳解

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

go build main

又報錯了!!!

Golang import本地包和匯入問題相關詳解

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

//main.go
package main

import (
  "hello_a"
)

func main() {
  hello_a.SayHello()
  hello_a.SayWorld()
}

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

go build main

繼續報錯!

Golang import本地包和匯入問題相關詳解

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

import (
  "hello"
)

再次編譯:

go build main

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

Golang import本地包和匯入問題相關詳解

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

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

(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")
}

並且更改main.go的原始碼如下

//main.go
package main

import (
  "hello"
)

func main() {
  hello.SayHello()
  hello_a.SayWorld()
}

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

go clean -i hello
go clean -x main

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

Golang import本地包和匯入問題相關詳解

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

go install hello

oh~~果然出錯了!!

Golang import本地包和匯入問題相關詳解
看到了嗎?它說它找到了兩個包了啊!!!

那這能說明什麼呢??

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

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

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

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

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

go help install

Golang import本地包和匯入問題相關詳解

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

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

2 包的匯入問題

匯入包:

  • 標準包使用的是給定的短路徑,如"fmt"、"net/http"
  • 自己的包,需要在工作目錄(GOPATH)下指定一個目錄,improt 匯入包,實際上就是基於工作目錄的資料夾目錄

匯入包的多種方式:

  • 直接根據$GOPATH/src目錄匯入import "test/lib"(路徑其實是$GOPATH/src/test/lib)
  • 別名匯入:import alias_name "test/lib" ,這樣使用的時候,可以直接使用別名
  • 使用點號匯入:import . "test/lib",作用是使用的時候直接省略包名
  • 使用下劃線匯入:improt _ "test/lib",該操作其實只是引入該包。當匯入一個包時,它所有的init()函式就會被執行,但有些時候並非真的需要使用這些包,僅僅是希望它的init()函式被執行而已。這個時候就可以使用_操作引用該包。即使用_操作引用包是無法通過包名來呼叫包中的匯出函式,而是隻是為了簡單的呼叫其init函式()。往往這些init函式裡面是註冊自己包裡面的引擎,讓外部可以方便的使用,例如實現database/sql的包,在init函式裡面都是呼叫了sql.Register(name string,driver driver.Driver)註冊自己,然後外部就可以使用了。
  • 相對路徑匯入 import "./model" //當前檔案同一目錄的model目錄,但是不建議這種方式import

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

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

hello.go

//hello.go
package hello

import (
  "fmt"
)

func SayHello() {
  fmt.Println("SayHello()-->Hello")
}

main.go

//main.go
package main

import (
  "hello"
)

func main() {
  hello.SayHello()
}

最後,讓整體保持如下的樣式:

Golang import本地包和匯入問題相關詳解

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

go install hello
go build main
./main

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

Golang import本地包和匯入問題相關詳解

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

//hello.go
package hello

import (
  "fmt"
)

func SayHello() {
  fmt.Println("SayHello()-->Hello_modifi...")
}

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

rm main
go build main

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

Golang import本地包和匯入問題相關詳解

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

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

rm main
go build -x -v main

結果:

Golang import本地包和匯入問題相關詳解

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

#首先,它好像指定了一個臨時工作目錄
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/hello.a -trimpath $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/main.a -trimpath $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/main/_obj/exe/a.out -L $WORK -L /home/yuxuan/GoProjects/import/pkg/linux_amd64 -extld=gcc $WORK/main.a

#最後,移動可執行檔案並重命名
mv $WORK/main/_obj/exe/a.out main

到這裡,其實差不多也就得出結論了,聯結器在連線時,其實使用的並不是我們工作目錄下的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檔案包匯入有三種模式:正常模式、別名模式、簡便模式

到此這篇關於Golang import本地包和匯入問題相關詳解的文章就介紹到這了,更多相關Golang import包內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!