Go語(yǔ)言包管理進(jìn)階

基礎(chǔ)

Go程序通過(guò)包(package)進(jìn)行組織,一個(gè)包可以由多個(gè)文件組成,但這些文件必須位于同一目錄下。每個(gè)文件通過(guò)在首行用package語(yǔ)句聲明所屬的包,例如package math,包名不要求一定要與目錄名相同(雖然通常會(huì)使用相同的)。 同一個(gè)包下定義的常量、類(lèi)型、變量和函數(shù)都是互相可見(jiàn)的,即使位于不同的文件中。大寫(xiě)字母開(kāi)頭的元素可以導(dǎo)出到其它包中使用。在這種約束的工程結(jié)構(gòu)組織下,編譯器無(wú)需額外指令(通常是寫(xiě)一個(gè)Makefile文件)就清楚知道怎樣構(gòu)建程序。編譯的時(shí)候每個(gè)包會(huì)生成一個(gè).a文件,可在build時(shí)通過(guò)-work參數(shù)打印臨時(shí)路徑查看,這些.a文件再鏈接生成最終的可執(zhí)行文件。要引入外部包,通過(guò)import語(yǔ)句,例如import math / import github.com/boltdb/bolt。import的參數(shù)是包路徑。關(guān)于GOPATH的管理,一種做法是設(shè)置一個(gè)唯一路徑,另一種做法是為每個(gè)項(xiàng)目單獨(dú)設(shè)置,我推薦使用第一種。

如果工程的輸出是可執(zhí)行文件則必需有main包。對(duì)于僅設(shè)計(jì)為內(nèi)部實(shí)現(xiàn)使用,而不是被外部引用的包可以放到internal目錄中,這樣位于internal目錄外的包就不能引用其中的包,否則會(huì)編譯錯(cuò)誤。internal的設(shè)計(jì)可以防止內(nèi)部實(shí)現(xiàn)細(xì)節(jié)擴(kuò)散到外部。

有四種導(dǎo)入(import)包的方式:

1. import   "path/pkg"
2. import x "path/pkg"
3. import . "path/pkg"
4. import _ "path/pkg"

假設(shè)包名和目錄名一致(此處假設(shè)為pkg),使用第1種導(dǎo)入方式,在使用時(shí)必須顯式的使用包名作為限定符(說(shuō)明名字空間),第2種方式使用指定的名字x作為限定符,第3種方式導(dǎo)入的包不需要使用限定符(導(dǎo)入包和當(dāng)前包在同一名稱(chēng)空間,要注意名字沖突問(wèn)題),第4種方式不導(dǎo)入名字空間,只是對(duì)導(dǎo)入包進(jìn)行初始化操作。最好不要使用第3種導(dǎo)入方式,否則當(dāng)工程變大時(shí)將很難維護(hù)。

引入外部包的問(wèn)題和解決辦法

通常稍大一點(diǎn)規(guī)模的項(xiàng)目都會(huì)引入外部包(例如數(shù)據(jù)庫(kù)驅(qū)動(dòng)) ,而不是把每個(gè)輪子都造一遍(不必要/成本不允許/根本沒(méi)造輪子的能力)。外部即意味著不在自己的控制之內(nèi),可能產(chǎn)生新舊版本的API不兼容問(wèn)題,例如函數(shù)增加了參數(shù)/結(jié)構(gòu)體字段類(lèi)型修改,甚至外部包的作者直接把項(xiàng)目刪除了!這就可能導(dǎo)致在你本機(jī)可以編譯通過(guò)并正常運(yùn)行的程序在新同事那里連編譯都通不過(guò)的情況發(fā)生。其中一種解決辦法是把用到的外部項(xiàng)目fork一份,但又會(huì)存在升級(jí)維護(hù)的問(wèn)題,而且這種方式給人的直觀感受就是丑陋的。我曾經(jīng)遇到過(guò)mongodb驅(qū)動(dòng)的不同版本支持的document最大size是不同的,導(dǎo)致遭遇更新驅(qū)動(dòng)版本后數(shù)據(jù)庫(kù)操作失敗的問(wèn)題。

Go工具鏈在1.5版本增加了實(shí)驗(yàn)性質(zhì)的vendor機(jī)制(通過(guò)GO15VENDOREXPERIMENT環(huán)境變量開(kāi)啟)來(lái)解決包依賴關(guān)系的問(wèn)題,從Go1.6開(kāi)始默認(rèn)開(kāi)啟,Go1.7成為標(biāo)準(zhǔn)特性。但是Go官方并沒(méi)有提供相關(guān)工具,有很多的第三方實(shí)現(xiàn)。通過(guò)綜合評(píng)估各個(gè)工具的熱度(github star / 在開(kāi)源項(xiàng)目中的使用情況)和易用性,推薦掌握govendorgodep (按先后優(yōu)先級(jí))。

govendor

govendor的基本原理就是通過(guò)vendor.json文件描述來(lái)確定工程使用的外部包。看一個(gè)簡(jiǎn)單示例

{
    "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可以保證不同的構(gòu)建者使用相同的外部包build工程,進(jìn)而保證可重復(fù)的確定輸出。通過(guò)FAQ可以快速掌握govendor的常用命令,此處不再贅述(可另起一篇介紹)。使用govendor命令可以在工程根目錄下增加vendor目錄,依賴的外部包可以通過(guò)命令拷貝一份,并且還可以通過(guò)命令升級(jí)維護(hù)。例如示例項(xiàng)目的vendor目錄結(jié)構(gòu)為:

vendor/
       vendor.json
       github.com/
                boltdb/
                    bolt/
       caojun.xyz/
                upid/

任何情況都把vendor.json簽入(check in)版本控制系統(tǒng)中,vendor目錄下的外部包拷貝通常根據(jù)情況決定是否簽入版本控制,main包下的vendor外部包就簽入,否則不簽入。這里很容易理解,這樣可以防止大量的重復(fù)代碼。

godep

我在使用godep的過(guò)程中遇到一個(gè)問(wèn)題,目前還沒(méi)有被close,以后再單獨(dú)寫(xiě)一篇介紹。

使用gopkg.in管理github開(kāi)源包

有很多被廣泛使用的github開(kāi)源項(xiàng)目通過(guò)gopkg.in進(jìn)行版本管理,例如mgoyaml。gopkg.in非侵入式的設(shè)計(jì)堪稱(chēng)巧妙,非常具有借鑒意義。它的設(shè)計(jì)建立在對(duì)版本號(hào)的管理約定和go get命令對(duì)http響應(yīng)meta信息的處理上。

采用三段式的版本號(hào)設(shè)計(jì):(vMAJOR[.MINOR[.PATCH]]),例如v1, v2, v2.0, v2.1.3。這里最重要的是主版本號(hào)(MAJOR)的變更,這往往意味著向后不兼容的修改。主版本號(hào)0表示不穩(wěn)定版本,github上相應(yīng)項(xiàng)目如果沒(méi)有任何滿足約定的tag或branch時(shí)默認(rèn)為v0,對(duì)應(yīng)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)

第一種樣式更精簡(jiǎn),通常用于被廣泛使用的有較大影響力的開(kāi)源項(xiàng)目,例如gopkg.in/yaml.v2,它通過(guò)包名和user名的名字約定來(lái)精簡(jiǎn)樣式。第二種樣式通常用于個(gè)人項(xiàng)目,例如pkg.in/caojunxyz/upid.v0。

以gopkg.in/yaml.v2為例說(shuō)明大致原理:

  1. 本地運(yùn)行go get gopkg.in/yaml.v2命令,發(fā)起https://gopkg.in/go-yaml/yaml.v1?go-get=1請(qǐng)求
  2. gopkg.in服務(wù)器收到請(qǐng)求后解析出目標(biāo)項(xiàng)目名yaml和目標(biāo)版本號(hào)v2
  3. gopkg.in服務(wù)器到github服務(wù)器查詢go-yaml/yaml項(xiàng)目是否存在,且存在名為v2tagbranch
  4. gopkg.in服務(wù)器在響應(yīng)go get的meta信息中包含源代碼clone地址和GOPATH中的對(duì)應(yīng)下載目錄
  5. go get克隆代碼到本地

這個(gè)例子中meta信息為:
<meta name="go-import" content="gopkg.in/yaml.v2 git https://github.com/go-yaml/yaml.git">

如果在瀏覽器中打開(kāi)gopkg.in/yaml.v2,系統(tǒng)會(huì)自動(dòng)生成一個(gè)web頁(yè)面,其中包含所有可用版本。通過(guò)"Source Code"超鏈接可以跳轉(zhuǎn)到項(xiàng)目的github地址,通過(guò)"API Documentation"超鏈接可以跳轉(zhuǎn)到項(xiàng)目在godog.org的對(duì)應(yīng)文檔頁(yè)面。godog.org也是一個(gè)很巧妙的設(shè)計(jì),有點(diǎn)類(lèi)似gopkg.in,它通過(guò)godoc命令生成項(xiàng)目文檔。

go get使用自定義域名

有時(shí)候我們可能會(huì)有通過(guò)自定義域名引用包的需求,比如公司內(nèi)部的項(xiàng)目。這種情況下,源代碼可能通過(guò)自建倉(cāng)庫(kù)(例如使用GitLab, Gogs, Github企業(yè)版等)托管或者托管在第三方的私有倉(cāng)庫(kù)中。go get默認(rèn)是不支持從自定義域名參數(shù)獲取代碼的,有一種做法是修改Go源碼實(shí)現(xiàn)該功能(很容易就實(shí)現(xiàn)了),網(wǎng)上也有人是這樣做的,但這種方式是侵入式的,Go官方默認(rèn)不支持自有其道理。更加優(yōu)雅的方式是通過(guò)類(lèi)似gopkg.in的方式,直接上代碼:

const domain = "caojun.xyz"

// const host = "https://github.com/caojunxyz" // 托管在github
const host = "http://caojun.xyz:3000"          // Gogs自建倉(cāng)庫(kù)

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 {}
}

這里需要注意的是,如果自定義域名已經(jīng)有web應(yīng)用運(yùn)行(公司主頁(yè))該如何處理:

  • 單獨(dú)使用一個(gè)子域名例如code.caojun.xyz(推薦)
  • web端檢測(cè)客戶端是否瀏覽器發(fā)起的請(qǐng)求,如果不是才返回go-import meta信息
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容