Golong學習之語言包管理進階
基礎
Go程式通過包(package)進行組織,一個包可以由多個檔案組成,但這些檔案必須位於同一目錄下。每個檔案通過在首行用package語句宣告所屬的包,例如package math,包名不要求一定要與目錄名相同(雖然通常會使用相同的)。 同一個包下定義的常量、型別、變數和函式都是互相可見的,即使位於不同的檔案中。大寫字母開頭的元素可以匯出到其它包中使用。在這種約束的工程結構組織下,編譯器無需額外指令(通常是寫一個Makefile檔案)就清楚知道怎樣構建程式。編譯的時候每個包會生成一個.a檔案,可在build時通過-work引數列印臨時路徑檢視,這些.a檔案再連結生成最終的可執行檔案。要引入外部包,通過import
import math / import github.com/boltdb/bolt
。import的引數是包路徑。關於GOPATH的管理,一種做法是設定一個唯一路徑,另一種做法是為每個專案單獨設定,我推薦使用第一種。
如果工程的輸出是可執行檔案則必需有main包。對於僅設計為內部實現使用,而不是被外部引用的包可以放到internal目錄中,這樣位於internal目錄外的包就不能引用其中的包,否則會編譯錯誤。internal的設計可以防止內部實現細節擴散到外部。
有四種匯入(import)包的方式:
1. import "path/pkg"
2. import x "path/pkg"
3. import . "path/pkg"
4. import _ "path/pkg"
假設包名和目錄名一致(此處假設為pkg),使用第1種匯入方式,在使用時必須顯式的使用包名作為限定符(說明名字空間),第2種方式使用指定的名字x作為限定符,第3種方式匯入的包不需要使用限定符(匯入包和當前包在同一名稱空間,要注意名字衝突問題),第4種方式不匯入名字空間,只是對匯入包進行初始化操作。最好不要使用第3種匯入方式,否則當工程變大時將很難維護。
引入外部包的問題和解決辦法
通常稍大一點規模的專案都會引入外部包(_
例如資料庫驅動) ,而不是把每個輪子都造一遍(不必要/成本不允許/根本沒造輪子的能力_
Go工具鏈在1.5版本增加了實驗性質的vendor機制(通過GO15VENDOREXPERIMENT
環境變數開啟)來解決包依賴關係的問題,從Go1.6開始預設開啟,Go1.7成為標準特性。但是Go官方並沒有提供相關工具,有很多的第三方實現。通過綜合評估各個工具的熱度(github star / 在開源專案中的使用情況)和易用性,推薦掌握govendor和godep (按先後優先順序)。
govendor
govendor的基本原理就是通過vendor.json檔案描述來確定工程使用的外部包。看一個簡單示例:
{
"comment": "",
"ignore": "test",
"package": [
{
"checksumSHA1": "Vw77VGlwiPNNoCPc+lKVeQWcKK4=",
"path": "github.com/boltdb/bolt",
"revision": "4b1ebc1869ad66568b313d0dc410e2be72670dda",
"revisionTime": "2016-10-28T19:36:45Z"
},
{
"checksumSHA1": "Jl0BawxPBuKr2uY1FpdXGyfCzrA=",
"path": "github.com/caojunxyz/upid",
"revision": "f8f05b4acc042cfc1a81bc9dbecb5232800d974b",
"revisionTime": "2016-10-12T11:57:35Z"
}
],
"rootPath": "github.com/caojunxyz/govendortest"
}
基於vendor.json可以保證不同的構建者使用相同的外部包build工程,進而保證可重複的確定輸出。通過FAQ可以快速掌握govendor的常用命令,此處不再贅述(可另起一篇介紹)。使用govendor命令可以在工程根目錄下增加vendor目錄,依賴的外部包可以通過命令拷貝一份,並且還可以通過命令升級維護。例如示例專案的vendor目錄結構為:
vendor/
vendor.json
github.com/
boltdb/
bolt/
caojun.xyz/
upid/
任何情況都把vendor.json簽入(check in)版本控制系統中,vendor目錄下的外部包拷貝通常根據情況決定是否簽入版本控制,main包下的vendor外部包就簽入,否則不簽入。這裡很容易理解,這樣可以防止大量的重複程式碼。
godep
我在使用godep的過程中遇到一個問題,目前還沒有被close,以後再單獨寫一篇介紹。
使用gopkg.in管理github開源包
有很多被廣泛使用的github開源專案通過gopkg.in進行版本管理,例如mgo、yaml。gopkg.in非侵入式的設計堪稱巧妙,非常具有借鑑意義。它的設計建立在對版本號的管理約定和go get命令對http響應meta資訊的處理上。
採用三段式的版本號設計:(vMAJOR[.MINOR[.PATCH]])
,例如v1, v2, v2.0, v2.1.3。這裡最重要的是主版本號(MAJOR)的變更,這往往意味著向後不相容的修改。主版本號0表示不穩定版本,github上相應專案如果沒有任何滿足約定的tag或branch時預設為v0,對應master分支。
meta資訊的格式為<meta name="go-import" content="pkg git repo">
。
gopkg.in支援兩種URL樣式,例如:
gopkg.in/pkg.v3 → github.com/go-pkg/pkg (branch/tag v3, v3.N, or v3.N.M)
gopkg.in/user/pkg.v3 → github.com/user/pkg (branch/tag v3, v3.N, or v3.N.M)
第一種樣式更精簡,通常用於被廣泛使用的有較大影響力的開源專案,例如gopkg.in/yaml.v2,它通過包名和user名的名字約定來精簡樣式。第二種樣式通常用於個人專案,例如pkg.in/caojunxyz/upid.v0。
以gopkg.in/yaml.v2為例說明大致原理:
- gopkg.in伺服器收到請求後解析出目標專案名yaml和目標版本號v2
- gopkg.in伺服器到github伺服器查詢go-yaml/yaml專案是否存在,且存在名為v2的tag或branch
- gopkg.in伺服器在響應go get的meta資訊中包含原始碼clone地址和GOPATH中的對應下載目錄
- go get克隆程式碼到本地
這個例子中meta資訊為:
<meta name="go-import" content="gopkg.in/yaml.v2 git https://github.com/go-yaml/yaml.git">
如果在瀏覽器中開啟gopkg.in/yaml.v2
,系統會自動生成一個web頁面,其中包含所有可用版本。通過”Source Code
“超連結可以跳轉到專案的github地址,通過”API Documentation”超連結可以跳轉到專案在godog.org的對應文件頁面。godog.org也是一個很巧妙的設計,有點類似gopkg.in,它通過godoc命令生成專案文件。
go get使用自定義域名
有時候我們可能會有通過自定義域名引用包的需求,比如公司內部的專案。這種情況下,原始碼可能通過自建倉庫(例如使用GitLab, Gogs, Github企業版等)託管或者託管在第三方的私有倉庫中。go get預設是不支援從自定義域名引數獲取程式碼的,有一種做法是修改Go原始碼實現該功能(很容易就實現了),網上也有人是這樣做的,但這種方式是侵入式的,Go官方預設不支援自有其道理。更加優雅的方式是通過類似gopkg.in的方式,直接上程式碼:
const domain = "caojun.xyz"
// const host = "https://github.com/caojunxyz" // 託管在github
const host = "http://caojun.xyz:3000" // Gogs自建倉庫
func handler(w http.ResponseWriter, r *http.Request) {
list := strings.Split(r.URL.Path, "/")
if len(list) > 1 {
repo := strings.Join(list[1:], "/")
content := fmt.Sprintf("%s/%s", domain, repo)
meta := fmt.Sprintf(`<meta name="go-import" content="%s git %s/%s.git">`, content, host, repo)
fmt.Println("meta:", meta)
fmt.Fprint(w, meta)
}
}
func main() {
http.HandleFunc("/", handler)
http.ListenAndServe(":80", nil)
select {}
}
這裡需要注意的是,如果自定義域名已經有web應用執行(公司主頁)該如何處理:
- 單獨使用一個子域名例如code.caojun.xyz(推薦)
- web端檢測客戶端是否瀏覽器發起的請求,如果不是才返回go-import meta資訊