1. 程式人生 > >Golong學習之語言包管理進階

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種匯入方式,否則當工程變大時將很難維護。

引入外部包的問題和解決辦法

       通常稍大一點規模的專案都會引入外部包(_例如資料庫驅動) ,而不是把每個輪子都造一遍(不必要/成本不允許/根本沒造輪子的能力_

)。外部即意味著不在自己的控制之內,可能產生新舊版本的API不相容問題,例如函式增加了引數/結構體欄位型別修改,甚至外部包的作者直接把專案刪除了!這就可能導致在你本機可以編譯通過並正常執行的程式在新同事那裡連編譯都通不過的情況發生。其中一種解決辦法是把用到的外部專案fork一份,但又會存在升級維護的問題,而且這種方式給人的直觀感受就是醜陋的。我曾經遇到過mongodb驅動的不同版本支援的document最大size是不同的,導致遭遇更新驅動版本後資料庫操作失敗的問題。

       Go工具鏈在1.5版本增加了實驗性質的vendor機制(通過GO15VENDOREXPERIMENT環境變數開啟)來解決包依賴關係的問題,從Go1.6開始預設開啟,Go1.7成為標準特性。但是Go官方並沒有提供相關工具,有很多的第三方實現。通過綜合評估各個工具的熱度(github star / 在開源專案中的使用情況)和易用性,推薦掌握govendorgodep (按先後優先順序)。

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進行版本管理,例如mgoyaml。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為例說明大致原理:

  1. gopkg.in伺服器收到請求後解析出目標專案名yaml和目標版本號v2
  2. gopkg.in伺服器到github伺服器查詢go-yaml/yaml專案是否存在,且存在名為v2的tag或branch
  3. gopkg.in伺服器在響應go get的meta資訊中包含原始碼clone地址和GOPATH中的對應下載目錄
  4. 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資訊