最近Go 1.11加入go mod的支援,對長期以來Go套件相依管理的問題,算是正式內建官方解決方案。go mod的前身是vgo,然而曾視為官方候選的dep?之前的glide、godep?vendor又是什麼?GOPATH怎麼了?

只有GOPATH?

想要認識go mod支援了什麼,就必須瞭解GOPATH做了什麼。如果輸入go help gopath指令,顯示的第一句話,就說明了一切「Go路徑用來解析import陳述(The Go path is used to resolve import statements)」開發者如果想知道import的非標準套件從何而來,儘管查找GOPATH就對了,至於自己開發的套件,或者是go get下載的套件,也是放在GOPATH(的src)之中。

在只有一個專案的情況下,GOPATH非常合情合理而且簡單,如果有多個專案,各個專案的原始碼也可以放在同一個GOPATH之中,有著各自的套件結構,使用著來自GOPATH的非標準套件,此時整個GOPATH目錄就是一個巨大的repository,具稱Google內部就是這樣的場景,才會有GOPATH這樣的設計,Go社群中也有著「如果必須切換GOPATH,大概有哪些地方不對了」的說法。

問題在於,這並不是社群或其他公司使用Go的方式。如果個別專案有個別套件,較單純的做法是,各專案有專用GOPATH,想要開發哪個專案,就切換至該專案用的GOPATH。然而,如果很快有專案相依在這些專案呢?將它們組織為巨大的repository是個做法,或者令GOPATH=prj1:prj2:prj3,當中的prjx是指向各專案原始碼的路徑,也就是說,GOPATH會是一大串路徑結合後的產物。

在上述設定中,維持一個GOPATH不用切換,新專案可加入GOPATH最前頭,go get的第三方套件會下載到最前面的路徑;然而,若需要prj2也在開發中,而prj2需要新的第三方套件時,go get卻會下載到新專案之中;在各自不同的情境中,無論怎麼調整GOPATH的順序,總是會有不同的問題發生。

另一方面,GOPATH本身僅用於import陳述,並不涉及套件來源的版本問題,因此,若專案依賴的repository被修改了,日後建構專案就會受到影響,對於依賴GitHub之類來源,而且第三方套件本身非常活躍的專案來說,重新建構專案時無法有穩定的結果。這顯然是個大問題。

複製一些程式碼又如何?

簡單來說,如果開發者的環境並非一個巨大的repository,GOPATH在本身的切換、版本控制、相依套件的處理上,就非常捉襟見肘。為了要能處理這些問題,社群相繼開發了套件管理工具,2013年誕生的Gedep是最為人所知的代表之一,原理也非常簡單,就是透過工具程式,把下載的相依套件原始碼,複製到專案中Godeps/_workspace目錄,這等同於將原始碼做了一份快照(Snapshot)。

要使用的話有幾種方式,其中之一是直接修改import陳述,像是修改原先的import "github.com/JustinSDK/pkgfoo",而成為import "mypkg/Godeps/_workspace/src/github.com/JustinSDK/pkgfoo",這很麻煩;另一方式是透過godep指令來包裹go指令,調整GOPATH至專案的Godeps/_workspace,由於各套件專案維持需要的相依套件,從而使得建構專案時,可以產生穩定的結果。

此外Go 1.5實驗性加入vendor特性,需透過GO15VENDOREXPERIMENT="1" 來啟用,在1.6預設為GO15VENDOREXPERIMENT="1",到了1.7,則是拿掉GO15VENDOREXPERIMENT環境變數,使vendor成為正式的內建特性,因此,如果套件裡有個vendor資料夾,對於import "github.com/JustinSDK/pkgfoo"來說,尋找相依套件的順序,將變成vendor->GOROOT的src->GOPATH的src。

也就是說,必須複製的相依套件,開始有了公開且一致的名稱及位置,而後來godep等工具,也都相繼支援vendor,然而複製的目的地雖然都是vendor了,受到複製的套件只是維持住當時的版本,這就使得後續升級、臭蟲修正等維護成了麻煩,另外,若第三方套件也相依於其他套件,該如何得知與下載等問題也必須解決。

Go 1.11 go mod

在Go支援vendor之後,有不少工具基於它而發展出來,其中之一是glide,我們可以在glide.yaml定義相關套件版本資訊,而glide也可以解析套件中的imports,記錄版本、下載相依套件等,從而解決了不少套件相依方面的問題。

Go官方文件〈PackageManagementTools〉(https://goo.gl/mXnYsZ)中列出的套件管理工具,就有多達15個專案,為了有一致的套件管理方式,2016年GopherCon大會之後,Go官方組織了社群委員會,在一連串討論後啟動了dep專案,而後於2017年初對外正式發布。

dep曾被認為將成為官方套件管理工具,也一度預計於Go 1.10納入go工具鏈,甚至Russ Cox在郵件討論(https://goo.gl/Z9hkgg)也建議大家使用dep,然而,2018年Russ Cox開發了vgo專案,並於〈A Proposal for Package Versioning in Go〉(https://goo.gl/NTwTzh)介紹,並請大家給予意見。

突然之間,官方有了dep與vgo兩個套件管理工具,在iThome報導的〈試試語意輸入版本控制的vgo〉(https://goo.gl/ncBMKs)已經整理了一些重點,表面是語意輸入版本控制(Semantic Import Versioning)支援問題,使得Russ Cox對於缺乏對大型程式支援感到擔心,然而,在Go 1.11正式將vgo合併成為go mod之際,Russ Cox與dep主要開發者Sam Boyer,以及社群成員發生了一連串爭論,牽扯出一些溝通上,以及Go官方面對社群時的態度等問題。

對dep與vgo間的論戰有興趣的話,可以看看〈關於Go Module的爭吵〉(https://goo.gl/5VRtCV)的整理。無論如何,Go 1.11內建的go mod是go工具鏈的一部份了,目前透過GO111MODULE環境變數來控制,預設值為auto,在這個模式下,若處於GOPATH之中,會忽略用來記錄版本及相依性的go.mod檔案,而以舊式的GOPATH及vendor方式運作;若是在GOPATH之外,就使用go.mod;而將GO111MODULE設為off或on,則是直接切換為舊式與新的模組管理。

目前,go mod使用版本控制的tag來表示套件的版本,可用Semantic Versioning(https://semver.org/),例如go mod的一條記錄,可能是require github.com/Justin/foopkg v1.0.1;另一個方式是Import Versioning,也就是在import的套件包含版本號,例如,import "github.com/Justin/foopkg/v2"。

由於Go兩者都支援,Russ Cox稱為Semantic Import Versioning,並在〈Introduction to Go Modules〉(https://goo.gl/aYUNgt),示範這兩種方式;就目前來說,下載套件會放在GOPATH/pkg/mod,而且,不同版本的套件在目錄名稱上,會有額外的版本訊息,像是github.com/!justin!s!d!k/goexample2@v1.0.1/foopkg@v1.0.1。

瞭解模組發展歷程

從某些程度來說,Go模組的發展過程,混亂程度不下於JavaScript模組的發展過程(可參考先前專欄〈JavaScript模組之路〉),就簡單區分來看,Go在1.11之前,使用老牌的godep或者是功能更豐富的glide等,而Go 1.11開始使用go mod。

然而,真正重要的是瞭解發展模組過程中的各種考量──對於考量JavaScript模組管理方案時必須如此,對於Go模組管理方案的考量也是如此。畢竟目前go mod還只是實驗性質,未來還有更多細節(像是移除對GOPATH、vendor的依賴等),而在這個青黃不接的階段,瞭解Go的模組發展歷程,會遠比瞭解個別模組管理工具來得重要!

專欄作者

熱門新聞

Advertisement