# go get
```go
hc@ubt:~$ go get github.com/hyper-carrot/go_lib/logging
```
命令`go get`可以根據要求和實際情況從互聯網上下載或更新指定的代碼包及其依賴包,并對它們進行編譯和安裝。在上面這個示例中,我們從著名的代碼托管站點Github上下載了一個項目(或稱代碼包),并安裝到了環境變量GOPATH中包含的第一個工作區中。與此同時,我們也知道了這個代碼包的導入路徑就是github.com/hyper-carrot/go_lib/logging。
一般情況下,為了分離自己與第三方的代碼,我們會設置兩個或更多的工作區。我們現在有一個目錄路徑為/home/hc/golang/lib的工作區,并且它是環境變量GOPATH值中的第一個目錄路徑。注意,環境變量GOPATH中包含的路徑不能與環境變量GOROOT的值重復。好了,如果我們使用`go get`命令下載和安裝代碼包,那么這些代碼包都會被安裝在上面這個工作區中。我們暫且把這個工作區叫做Lib工作區。在我們運行`go get github.com/hyper-carrot/go_lib/logging`之后,這個代碼包就應該會被保存在Lib工作的src目錄下,并且已經被安裝妥當,如下所示:
```go
/home/hc/golang/lib:
bin/
pkg/
linux_386/
github.com/
hyper-carrot/
go_lib/
logging.a
src/
github.com/
hyper-carrot/
go_lib/
logging/
...
```
另一方面,如果我們想把一個項目上傳到Github網站(或其他代碼托管網站)上并被其他人使用的話,那么我們就應該把這個項目當做一個代碼包來看待。其實我們在之前已經提到過原因,`go get`命令會將項目下的所有子目錄和源碼文件存放到第一個工作區的src目錄下,而src目錄下的所有子目錄都會是某個代碼包導入路徑的一部分或者全部。也就是說,我們應該直接在項目目錄下存放子代碼包和源碼文件,并且直接存放在項目目錄下的源碼文件所聲明的包名應該與該項目名相同(除非它是命令源碼文件)。這樣做可以讓其他人使用`go get`命令從Github站點上下載你的項目之后直接就能使用它。
實際上,像goc2p項目這樣直接以項目根目錄的路徑作為工作區路徑的做法是不被推薦的。之所以這樣做主要是想讓讀者更容易的理解Go語言的工程結構和工作區概念,也可以讓讀者看到另一種項目結構。當然,如果你的項目使用了[gb](https://github.com/constabulary/gb)這樣的工具那就是另外一回事了。這樣的項目的根目錄就應該被視為一個工作區(但是你不必把它加入到GOPATH環境變量中)。它應該由`git clone`下載到Go語言工作區之外的某處,而不是使用`go get`命令。
**遠程導入路徑分析**
實際上,`go get`命令所做的動作也被叫做代碼包遠程導入,而傳遞給該命令的作為代碼包導入路徑的那個參數又被叫做代碼包遠程導入路徑。
`go get`命令不僅可以從像Github這樣著名的代碼托管站點上下載代碼包,還可以從任何命令支持的代碼版本控制系統(英文為Version Control System,簡稱為VCS)檢出代碼包。任何代碼托管站點都是通過某個或某些代碼版本控制系統來提供代碼上傳下載服務的。所以,更嚴格地講,`go get`命令所做的是從代碼版本控制系統的遠程倉庫中檢出/更新代碼包并對其進行編譯和安裝。
該命令所支持的VCS的信息如下表:
_表0-2 ```go get```命令支持的VCS_
名稱 | 主命令 | 說明
---------- | ------- | -----
Mercurial | hg | Mercurial是一種輕量級分布式版本控制系統,采用Python語言實現,易于學習和使用,擴展性強。
Git | git | Git最開始是Linux Torvalds為了幫助管理 Linux 內核開發而開發的一個開源的分布式版本控制軟件。但現在已被廣泛使用。它是被用來進行有效、高速的各種規模項目的版本管理。
Subversion | svn | Subversion是一個版本控制系統,也是第一個將分支概念和功能納入到版本控制模型的系統。但相對于Git和Mercurial而言,它只算是傳統版本控制系統的一員。
Bazaar | bzr | Bazaar是一個開源的分布式版本控制系統。但相比而言,用它來作為VCS的項目并不多。
`go get `命令在檢出代碼包之前必須要知道代碼包遠程導入路徑所對應的版本控制系統和遠程倉庫的URL。
如果該代碼包在本地工作區中已經存在,則會直接通過分析其路徑來確定這幾項信息。`go get `命令支持的幾個版本控制系統都有一個共同點,那就是會在檢出的項目目錄中存放一個元數據目錄,名稱為“.”前綴加其主命令名。例如,Git會在檢出的項目目錄中加入一個名為“.git”的子目錄。所以,這樣就很容易判定代碼包所用的版本控制系統。另外,又由于代碼包已經存在,我們只需通過代碼版本控制系統的更新命令來更新代碼包,因此也就不需要知道其遠程倉庫的URL了。對于已存在于本地工作區的代碼包,除非要求強行更新代碼包,否則`go get`命令不會進行重復下載。如果想要強行更新代碼包,可以在執行`go get`命令時加入`-u`標記。這一標記會稍后介紹。
如果本地工作區中不存在該代碼包,那么就只能通過對代碼包遠程導入路徑進行分析來獲取相關信息了。首先,`go get`命令會對代碼包遠程導入路徑進行靜態分析。為了使分析過程更加方便快捷,`go get`命令程序中已經預置了幾個著名代碼托管網站的信息。如下表:
_表0-3 預置的代碼托管站點的信息_
名稱 | 主域名 | 支持的VCS | 代碼包遠程導入路徑示例
--------------------------- | --------------- | -------------------------- | --------
Bitbucket | bitbucket.org | Git, Mercurial | bitbucket.org/user/project<br>bitbucket.org/user/project/sub/directory
GitHub | github.com | Git | github.com/user/project<br>github.com/user/project/sub/directory
Google Code Project Hosting | code.google.com | Git, Mercurial, Subversion | code.google.com/p/project<br>code.google.com/p/project/sub/directory<br>code.google.com/p/project.subrepository<br>code.google.com/p/project.subrepository/sub/directory
Launchpad | launchpad.net | Bazaar | launchpad.net/project<br>launchpad.net/project/series<br>launchpad.net/project/series/sub/directory<br>launchpad.net/~user/project/branch<br>launchpad.net/~user/project/branch/sub/directory
IBM DevOps Services | hub.jazz.net | Git | hub.jazz.net/git/user/project<br>hub.jazz.net/git/user/project/sub/directory
一般情況下,代碼包遠程導入路徑中的第一個元素就是代碼托管網站的主域名。在靜態分析的時候,`go get`命令會將代碼包遠程導入路徑與預置的代碼托管站點的主域名進行匹配。如果匹配成功,則在對代碼包遠程導入路徑的初步檢查后返回正常的返回值或錯誤信息。如果匹配不成功,則會再對代碼包遠程導入路徑進行動態分析。至于動態分析的過程,我就不在這里詳細展開了。
如果對代碼包遠程導入路徑的靜態分析或/和動態分析成功并獲取到對應的版本控制系統和遠程倉庫URL,那么`go get`命令就會進行代碼包檢出或更新的操作。隨后,`go get`命令會在必要時以同樣的方式檢出或更新這個代碼包的所有依賴包。
**自定義代碼包遠程導入路徑**
如果你想把你編寫的(被托管在不同的代碼托管網站上的)代碼包的遠程導入路徑統一起來,或者不希望讓你的代碼包中夾雜某個代碼托管網站的域名,那么你可以選擇自定義你的代碼包遠程導入路徑。這種自定義的實現手段叫做“導入注釋”。導入注釋的寫法示例如下:
```go
package analyzer // import "hypermind.cn/talon/analyzer"
```
代碼包`analyzer`實際上屬于我的一個網絡爬蟲項目。這個項目的代碼被托管在了Github網站上。它的網址是:[https://github.com/hyper-carrot/talon](https://github.com/hyper-carrot/talon)。如果用標準的導入路徑來下載`analyzer`代碼包的話,命令應該這樣寫`go get github.com/hyper-carrot/talon/analyzer`。不過,如果我們像上面的示例那樣在該代碼包中的一個源碼文件中加入導入注釋的話,這樣下載它就行不通了。我們來看一看這個導入注釋。
導入注釋的寫法如同一條代碼包導入語句。不同的是,它出現在了單行注釋符`//`的右邊,因此Go語言編譯器會忽略掉它。另外,它必須出現在源碼文件的第一行語句(也就是代碼包聲明語句)的右邊。只有符合上述這兩個位置條件的導入注釋才是有效的。再來看其中的引號部分。被雙引號包裹的應該是一個符合導入路徑語法規則的字符串。其中,`hypermind.cn`是我自己的一個域名。實際上,這也是用來替換掉我想隱去的代碼托管網站域名及部分路徑(這里是`github.com/hyper-carrot`)的那部分。在`hypermind.cn`右邊的依次是我的項目的名稱以及要下載的那個代碼包的相對路徑。這些與其標準導入路徑中的內容都是一致的。為了清晰起見,我們再來做下對比。
```go
github.com/hyper-carrot/talon/analyzer // 標準的導入路徑
hypermind.cn /talon/analyzer // 導入注釋中的導入路徑
```
你想用你自己的域名替換掉標準導入路徑中的哪部分由你自己說了算。不過一般情況下,被替換的部分包括代碼托管網站的域名以及你在那里的用戶ID就可以了。這足以達到我們最開始說的那兩個目的。
雖然我們在talon項目中的所有代碼包中都加入了類似的導入注釋,但是我們依然無法通過`go get hypermind.cn/talon/analyzer`命令來下載這個代碼包。因為域名`hypermind.cn`所指向的網站并沒有加入相應的處理邏輯。具體的實現步驟應該是這樣的:
1. 編寫一個可處理HTTP請求的程序。這里無所謂用什么編程語言去實現。當然,我推薦你用Go語言去做。
2. 將這個處理程序與`hypermind.cn/talon`這個路徑關聯在一起,并總是在作為響應的HTML文檔的頭中寫入下面這行內容:
```html
<meta name="go-import" content="hypermind.cn/talon git https://github.com/hyper-carrot/talon">
```
hypermind.cn/talon/analyzer熟悉HTML的讀者都應該知道,這行內容會被視為HTML文檔的元數據。它實際上`go get`命令的文檔中要求的寫法。它的模式是這樣的:
```html
<meta name="go-import" content="import-prefix vcs repo-root">
```
實際上,`content`屬性中的`import-prefix`的位置上應該填入我們自定義的遠程代碼包導入路徑的前綴。這個前綴應該與我們的處理程序關聯的那個路徑相一致。而`vsc`顯然應該代表與版本控制系統有關的標識。還記得表0-2中的主命令列嗎?這里的填入內容就應該該列中的某一項。在這里,由于talon項目使用的是Git,所以這里應該填入`git`。至于`repo-root`,它應該是與該處理程序關聯的路徑對應的Github網站的URL。在這里,這個路徑是`hypermind.cn/talon`,那么這個URL就應該是`https://github.com/hyper-carrot/talon`。后者也是talon項目的實際網址。
好了,在我們做好上述處理程序之后,`go get hypermind.cn/talon/analyzer`命令的執行結果就會是正確的。`analyzer`代碼包及其依賴包中的代碼會被下載到GOPATH環境變量中的第一個工作區目錄的src子目錄中,然后被編譯并安裝。
注意,具體的代碼包源碼存放路徑會是/home/hc/golang/lib/src/hypermind.cn/talon/analyzer。也就是說,存放路徑(包括代碼包源碼文件以及相應的歸檔文件的存放路徑)會遵循導入注釋中的路徑(這里是`hypermind.cn/talon/analyzer`),而不是原始的導入路徑(這里是`github.com/hyper-carrot/talon/analyzer`)。另外,我們只需在talon項目的每個代碼包中的某一個源碼文件中加入導入注釋,但這些導入注釋中的路徑都必須是一致的。在這之后,我們就只能使用`hypermind.cn/talon/`作為talon項目中的代碼包的導入路徑前綴了。一個反例如下:
```go
hc@ubt:~$ go get github.com/hyper-carrot/talon/analyzer
package github.com/hyper-carrot/talon/analyzer: code in directory /home/hc/golang/lib/src/github.com/hyper-carrot/talon/analyzer expects import "hypermind.cn/talon/analyzer"
```
與自定義的代碼包遠程導入路徑有關的內容我們就介紹到這里。從中我們也可以看出,Go語言為了讓使用者的項目與代碼托管網站隔離所作出的努力。只要你有自己的網站和一個不錯的域名,這就很容易搞定并且非常值得。這會在你的代碼包的使用者面前強化你的品牌,而不是某個代碼托管網站的。當然,使你的代碼包導入路徑整齊劃一是最直接的好處。
OK,言歸正傳,我下面繼續關注`go get`這個命令本身。
**命令特有標記**
`go get`命令可以接受所有可用于`go build`命令和`go install`命令的標記。這是因為`go get`命令的內部步驟中完全包含了編譯和安裝這兩個動作。另外,`go get`命令還有一些特有的標記,如下表所示:
_表0-4 ```go get```命令的特有標記說明_
標記名稱 | 標記描述
--------- | -------
-d | 讓命令程序只執行下載動作,而不執行安裝動作。
-f | 僅在使用`-u`標記時才有效。該標記會讓命令程序忽略掉對已下載代碼包的導入路徑的檢查。如果下載并安裝的代碼包所屬的項目是你從別人那里Fork過來的,那么這樣做就尤為重要了。
-fix | 讓命令程序在下載代碼包后先執行修正動作,而后再進行編譯和安裝。
-insecure | 允許命令程序使用非安全的scheme(如HTTP)去下載指定的代碼包。如果你用的代碼倉庫(如公司內部的Gitlab)沒有HTTPS支持,可以添加此標記。請在確定安全的情況下使用它。
-t | 讓命令程序同時下載并安裝指定的代碼包中的測試源碼文件中依賴的代碼包。
-u | 讓命令利用網絡來更新已有代碼包及其依賴包。默認情況下,該命令只會從網絡上下載本地不存在的代碼包,而不會更新已有的代碼包。
為了更好的理解這幾個特有標記,我們先清除Lib工作區的src目錄和pkg目錄中的所有子目錄和文件。現在我們使用帶有`-d`標記的`go get`命令來下載同樣的代碼包:
```go
hc@ubt:~$ go get -d github.com/hyper-carrot/go_lib/logging
```
現在,讓我們再來看一下Lib工作區的目錄結構:
```go
/home/hc/golang/lib:
bin/
pkg/
src/
github.com/
hyper-carrot/
go_lib/
logging/
...
```
我們可以看到,`go get`命令只將代碼包下載到了Lib工作區的src目錄,而沒有進行后續的編譯和安裝動作。這個加入`-d`標記的結果。
再來看`-fix`標記。我們知道,絕大多數計算機編程語言在進行升級和演進過程中,不可能保證100%的向后兼容(Backward Compatibility)。在計算機世界中,向后兼容是指在一個程序或者代碼庫在更新到較新的版本后,用舊的版本程序創建的軟件和系統仍能被正常操作或使用,或在舊版本的代碼庫的基礎上編寫的程序仍能正常編譯運行的能力。Go語言的開發者們已想到了這點,并提供了官方的代碼升級工具——`fix`。`fix`工具可以修復因Go語言規范變更而造成的語法級別的錯誤。關于`fix`工具,我們將放在本節的稍后位置予以說明。
假設我們本機安裝的Go語言版本是1.5,但我們的程序需要用到一個很早之前用Go語言的0.9版本開發的代碼包。那么我們在使用`go get`命令的時候可以加入`-fix`標記。這個標記的作用是在檢出代碼包之后,先對該代碼包中不符合Go語言1.5版本的語言規范的語法進行修正,然后再下載它的依賴包,最后再對它們進行編譯和安裝。
標記`-u`的意圖和執行的動作都比較簡單。我們在執行`go get`命令時加入`-u`標記就意味著,如果在本地工作區中已存在相關的代碼包,那么就是用對應的代碼版本控制系統的更新命令更新它,并進行編譯和安裝。這相當于強行更新指定的代碼包及其依賴包。我們來看如下示例:
```go
hc@ubt:~$ go get -v github.com/hyper-carrot/go_lib/logging
```
因為我們在之前已經檢出并安裝了這個代碼包,所以我們執行上面這條命令后什么也沒發生。還記得加入標記`-v`標記意味著會打印出被構建的代碼包的名字嗎?現在我們使用標記`-u`來強行更新代碼包:
```go
hc@ubt:~$ go get -v -u github.com/hyper-carrot/go_lib/logging
github.com/hyper-carrot/go_lib (download)
```
其中,“(download)”后綴意味著命令從遠程倉庫檢出或更新了該行顯示的代碼包。如果我們要查看附帶`-u`的`go get`命令到底做了些什么,還可以加上一個`-x`標記,以打印出用到的命令。讀者可以自己試用一下它。
**智能的下載**
命令`go get`還有一個很值得稱道的功能。在使用它檢出或更新代碼包之后,它會尋找與本地已安裝Go語言的版本號相對應的標簽(tag)或分支(branch)。比如,本機安裝Go語言的版本是1.x,那么`go get`命令會在該代碼包的遠程倉庫中尋找名為“go1”的標簽或者分支。如果找到指定的標簽或者分支,則將本地代碼包的版本切換到此標簽或者分支。如果沒有找到指定的標簽或者分支,則將本地代碼包的版本切換到主干的最新版本。
前面我們說在執行`go get`命令時也可以加入`-x`標記,這樣可以看到`go get`命令執行過程中所使用的所有命令。不知道讀者是否已經自己嘗試了。下面我們還是以代碼包`github.com/hyper-carrot/go_lib`為例,并且通過之前示例中的命令的執行此代碼包已經被檢出到本地。這時我們再次更新這個代碼包:
```go
hc@ubt:~$ go get -v -u -x github.com/hyper-carrot/go_lib
github.com/hyper-carrot/go_lib (download)
cd /home/hc/golang/lib/src/github.com/hyper-carrot/go_lib
git fetch
cd /home/hc/golang/lib/src/github.com/hyper-carrot/go_lib
git show-ref
cd /home/hc/golang/lib/src/github.com/hyper-carrot/go_lib
git checkout origin/master
WORK=/tmp/go-build034263530
```
在上述示例中,`go get`命令通過`git fetch`命令將所有遠程分支更新到本地,而后有用`git show-ref`命令列出本地和遠程倉庫中記錄的代碼包的所有分支和標簽。最后,當確定沒有名為“go1”的標簽或者分支后,`go get`命令使用`git checkout origin/master`命令將代碼包的版本切換到主干的最新版本。下面,我們在本地增加一個名為“go1”的標簽,看看`go get`命令的執行過程又會發生什么改變:
```go
hc@ubt:~$ cd ~/golang/lib/src/github.com/hyper-carrot/go_lib
hc@ubt:~/golang/lib/src/github.com/hyper-carrot/go_lib$ git tag go1
hc@ubt:~$ go get -v -u -x github.com/hyper-carrot/go_lib
github.com/hyper-carrot/go_lib (download)
cd /home/hc/golang/lib/src/github.com/hyper-carrot/go_lib
git fetch
cd /home/hc/golang/lib/src/github.com/hyper-carrot/go_lib
git show-ref
cd /home/hc/golang/lib/src/github.com/hyper-carrot/go_lib
git show-ref tags/go1 origin/go1
cd /home/hc/golang/lib/src/github.com/hyper-carrot/go_lib
git checkout tags/go1
WORK=/tmp/go-build636338114
```
將這兩個示例進行對比,我們會很容易發現它們之間的區別。第二個示例的命令執行過程中使用`git show-ref`查看所有分支和標簽,當發現有匹配的信息又通過`git show-ref tags/go1 origin/go1`命令進行精確查找,在確認無誤后將本地代碼包的版本切換到標簽“go1”之上。
命令`go get`的這一功能是非常有用的。我們的代碼在直接或間接依賴某些同時針對多個Go語言版本開發的代碼包時,可以自動的檢出其正確的版本。也可以說,`go get`命令內置了一定的代碼包多版本依賴管理的功能。
到這里,我向大家介紹了`go get`命令的使用方式。`go get`命令與之前介紹的兩個命令一樣,是我們編寫Go語言程序、構建Go語言項目時必不可少的輔助工具。