Go Module 雜談

文章推薦指數: 80 %
投票人數:10人

Go module 類似於npm 的package.json,用一份go.mod 檔案維護所使用的lib 與使用的版本(require 內部的東西),在跑起來( go run 、 go mod download 、 go ... SearchChia-AnLee Chia-AnLeeHomeSkillsExperiencePostsProjectsPublicationsCVLight Dark AutomaticGoModule雜談Chia-AnLeeLastupdatedon Aug9,2021 6minread codingv3.0.5之後各NF使用gomodule維護所使用的libversion,這邊簡單紀錄gomodule原理、free5GC會面臨的問題、debug手法Golang原本沒有打算推出套件管理系統,因為據說Google內部並不會使用舊版lib,而是使用單一程式庫(MonoRepo)[1],若任何套件有跟新,就直接讓全部有使用此套件的軟體使用新版套件。

可是外部社群不能這樣玩,所以逐漸推出了各自的套件版本管理系統,諸如godep,gopkg.in,vgo等等。

但在gov1.11Golang社群(或是Google內部golang維護者)推出了gomodule系統,直接否定所有外部版本管理套件,因此當時也惹來了godep開發者的不滿與爭吵[2]。

Gomodule類似於npm的package.json,用一份go.mod檔案維護所使用的lib與使用的版本(require內部的東西),在跑起來(gorun、gomoddownload、goinstall、gobuild)的時候,就會直接用gogetpakcage@version的方式下載library,並且放到$GOPATH/pkg/mod資料夾下面。

安裝使用Gov1.11之後的版本皆有gomodule可以使用,可以用goenvGO111MODULE確認module模式設定,有on/off/auto三種,on預設都用gomodule,off都不用,auto會看資料夾以及資料夾遞迴上去有沒有go.mod的檔案,如果有就用gomodule,建議用auto就好,出問題再視情況goenv-wGO111MODULE=on/off設定修改。

Initializationgomodinit{modulename}來初始化gomodule,下完此指令後可以在資料夾下看到新增了一個go.mod的檔案,內容如下,modulename就是你自己定義的modulename,version預設會使用目前你的go版本module{modulename} go{goversion} 此時還不會出現require欄位與go.sum檔案,如果資料夾下已經有gofile,可以用gomodtidy做golib的polish,因為此指令會把go.modrequire中沒用到的lib刪掉,同時會把gofile內部有用到的lib放進go.mod中,因此可以用此指令幫忙直接更新go.mod(直接gorun或其他跑起來的指令,沒有被寫進go.mod的lib其實好像也會幫忙寫入)。

載入已使用之package下完gomodtidy後,會發現go.mod多了require欄位,標明所使用的lib與所使用的版本,因為之前沒有這個欄位,go預設會抓下指令時的最新版本,如果那個lib有打gittag,會抓最新的gittag當版本,如果沒打,會抓最新的commithash當版本module{modulename} go{goversion} require( gopkg.in/yaml.v2v2.3.0 github.com/wmnsk/go-gtpv0.7.4 github.com/bronze1man/radiusv0.0.0-20190516032554-afd8baec892d gopkg.in/check.v1v1.0.0-20200227125254-8fa46927fb4f//indirect ... ) 其中因為yaml.v2有上gittag,使用的版本是直接用gittag的v2.3.0,而radius沒有打任何gittag,因此版本使用v0.0.0後面接commit的時間-{commithash|cut-c1-12}[3]。

另外也有像是check.v1後面有註解//indirect,這代表這個package不是這份project本身有用到,但是是這個project所使用的lib有使用的package。

Package下載位置這些lib會被goget到$GOPATH/pkg/mod/{pakcagepath}@{version}下($GOPATH可以用goenvGOPATH看到),例如yaml就會被下載到$GOPATH/pkg/mod/gopkg.in/[email protected]/下面,若之後有升/降級用到其他版本,會重新抓到$GOPATH/pkg/mod/gopkg.in/[email protected]/下面。

lib檢查順序:檢查是否gomodule,如果有(go.mod/GO111MODULE),看packageprefix是否跟go.mod的modulename相同,若相同則知道視同個專案,拔除modulename後,postfix當作directorpath往下找檢查是否有vendorfolder在這個project下面,如果有,檢查vendor資料夾下是否有modules.txt檔案,若有,使用modules.txt所指向的package位置(在vendor資料夾裡面)檢查go.mod或modules.txt是否有replace,優先使用replace的lib位置modulenameprefix不相同,檢查$GOPATH/pkg/mod下是否有lib拿prefixURLgitclone看是否有東西檢查$GOPATH/src下是否有lib檢查$GOROOT下是否有libgo.sum另外,有require後,也會自動生出go.sum的檔案,這份檔案是用來維護lib的checksum做安全性與完整性的檢查,主要有兩種[4],差別在於這個package(lib)是否有go.mod檔案(是否是gomodule管理),有/go.mod的package代表這個lib本身沒有用gomodule管理。

/go.mod go.sum上面講的東西其實不太重要,更詳細的內容可以翻[4]有更多細節。

主要需要知道的是go.sum是對使用package做的checksum檢查,因為gomodule對於任何合法的url都可以使用(而不是像npm等作中心化管理),避免被竄改所以需要做checksum檢查。

另外go.sum還會紀錄之前有使用過的版本,因此一份陳年的go.sum內容會有很多版本(transparentlog)。

既然go.sum的設計是紀錄checksum,那就可能會帶出一些麻煩,常見有兩個問題在使用的package有forcepush/rebase/commit–amend等強迫性操作時,再配合gittag,gittag-d(刪除),gittag的操作(就是如果你要re-tag),會發生同一個tag在被re-tag後,checksum不同,可是對go.sum來說,只能知道checksum不吻合可能被竄改,便會拒絕使用package,這時候最簡單的作法是手動到go.sum內把當個package的checksum列全部刪掉,重新跑gorun,讓go.sum重抓,但更建議的是盡可能不要做git破壞性更新(套件開發者不要亂搞。

merge時因為兩個branch更新了同一個套件,又尤其當兩個branch跟新到的版本(commit)不同,便需要手動解conflict,大致有兩種解法,一是把曾用到的commit都納入go.sum(brancha跟branchb的commit都留著),因為go.sum的transparentlog性質,理論上曾經用過的版本都要留著;另外一種做法是選其中一個版本,確認可以work就好。

嫌麻煩更簡單的做法一樣是進到go.sum把相關的列都刪掉,重新gorun讓他自己抓。

也許你會覺得直接不commitgo.sum就好,可是因為gomodule不像npm信任一個中心化的套件管理倉儲,為了安全性與完整性,go.sum應該是必須被commit的,而不像npm的package-lock.json應該被ignore掉。

升級package可以用goget-u{package}來單獨升級package的minor/patchversion,可以用goget-u來升級全部package的minor/patchversion,可以用goget-u=patch只升級patch,不過這都是在有打gittag的情況。

可以用goget-u{package}@{version}來使用特定版本,其中version可以是gittag,branchname,commithash,latest[5]。

:::warning gomodule本身有一個指令gomodedit-require=path@version可以更新go.mod的版本,但是官方不建議這樣使用。

Notethat-requireoverridesanyexistingrequirementsonpath.Theseflagsaremainlyfortoolsthatunderstandthemodulegraph.Usersshouldprefer‘gogetpath@version’or‘gogetpath@none’,whichmakeothergo.modadjustmentsasneededtosatisfyconstraintsimposedbyothermodules. ::::::info 當過了一段時間(尤其在release前),建議用gomodtidy把沒在用的packagepolish掉,避免不必要的下載。

:::開發使用PrivateRepo問題由於我們內部的專案是privaterepo,直接goget會permissiondenied(403accessdenied),因此要處理權限問題。

由於goget本身是用git的方式抓package(可以用goget-x或gomoddownload-x看到詳細指令),因此理論上只要gitfetch可以抓的到code就可以了,常理來想就把ssh-key塞進gitserver,然後用git@的方式抓code,或者指定帳號密碼用https://ac:pw@的方式來抓code,但是可以看出這兩種方法抓code,都是需要改upstream的字串(從https://變成git@或https://ac:pw@),而goget或自動加https://在packagename前面(ex.github.com/free5gc/aper會被加成https://github.com/free5gc/aper.git),要如何讓他改成加git@等呢?這時候可以使用git的insteadOf操作,利用gitconfig--globalurl."[email protected]:free5gc/".insteadOfhttps://github.com/free5gc/ 來使git在抓code的時候自動改變url,而不需要在go上面改變操作。

其中一定要用--global參數,如果不下,這個參數只會被設定在localproject裡面,而goget的時候是不會吃到這個參數的。

這個設定會被寫入$HOME/.gitconfig這份檔案裡,如果需要可以直接去改這份檔案。

設定完git後似乎可行了,可是實際goget時還是會抓不到,那是因為goenv內有GOPROXY[6]與GOSUMDB[7]兩個參數,這兩個參數類似golang本身的可信任函式庫,非public的project因為無法透過這兩個proxy被獲取,因此會被視為危險被竄改的url,需要把privaterepourl列入GONOPROXY與GONOSUMDB參數內,而在go1.13版本之後,有GOPRIVATE[8]可以直接使用,它會幫忙同時寫入GONOPROXY與GONOSUMDB兩個參數。

最後,理論上GOPROXY參數預設就有direct在裡面,但是如果沒有,也需要加入Inshort,下兩個指令讓goget可以抓到privaterepo `gitconfig--globalurl.”[email protected]:free5gc/”.insteadOfhttps://github.com/free5gc/` `goenv-wGOPRIVATE=github.com/free5gc/*` 也可以直接用現成的script:`cdinfra/&&./infra.sh` Package(lib)開發問題在開發時,單獨看一個package都是開發完後推上gitserver做release,才讓別人使用。

但由於我們同時是package的producer與consumer,會出現如果把package(lib)開發完,推上gitserver,使用的NF才能更新go.mod的版本來使用,但是這樣會遇到無法快速迭代測試的問題,也就是如果你正在bugfixaper,你無法local修完直接用amf來測試(往好處想就是unittest要寫得更詳細🤔),而是需要先推到remoteserver上,然後更新amf的go.mod中apercommithash,才能來測試amf是否可以跑得起來,而如果發現aper有錯沒修好,就要反覆重複上面的動作。

:::warning 仔細講這裡會遇到更多討論,像是為了避免commitmessage變得沒有意義,應該用gitcommit--amend來更新,但是--amend是forcepush,會改變commithash與history,而這時候使用它的project(amf)因為有go.sum維護checksum,可能會發現history被改變了而不允許跟新,這時候可能還會需要進到go.sum內手動改內容… :::1.使用replace[9]在go.mod下使用replace可以把go.mod所使用的package替換成指定的路徑下的package,就不會再從網路上拉並且放到$GOPATH/pkg/mod/資料夾下面,用這個方法在修改指定路徑下的package後,更動可以直接apply到使用它的project上,使用上先clone你要改的package到你指定的localpath(之後才能commit回到gitserver),然後在go.mod中replacepackage到剛剛的位置(可以用相對或絕對路徑),然後進行修改gitclonepackage/to/local/path gomodedit-replacepackage=/to/local/path#新增replace,可以用相對或絕對路徑 gomodedit-fmt#formatgo.mod,理論上用指令改了話不需要做 gomodedit-dropreplacepackage#刪除replace 但replace本身有一個限制,無法replaceindirect的package,也就是說你無法replace你用到的package所使用到的package,ex.amf用到util_3gpp,而util_3gpp有用到crypto,如果amf本身沒用到crypto,它就不能replacecrypto,就算把replacecrypto寫入go.mod中也不會生效。

(如果改用2.vendor的做法,因為vendor等於把所有package用跟pkg/mod的方式一樣全部都抓下來,所以應該可以把修改的crypto應用進amf中)(replace不只可以把package改成localpath,也可以修改remotepackage位置,因此很多China的文件有用replace來bypass牆)2.使用vendor[10](據說vendor會在之後的某一個go版本被outdated)使用gomodvendor會把go.mod內所有的package下載到project內的vendor資料夾下,並且建立一個modules.txt的檔案,由於project有vendor資料夾,golib會優先看這個資料夾內的package,其中package的資料夾結構與$GOPATH/src的相似,可以透過這個性質,cd到要修改的package,刪掉packagefolder後重新clone一個下來(因為這個folder不會帶git,無法commit)(或用其他gitinit/set-url的方式),再行修改。

:::warning 要注意每次下gomodvendor指令,golang會直接把vendor資料夾刪除後建立新的資料夾,不會檢查內部是否有修改,因此要小心localchange一定要push上server。

:::ConsideringissueGomod統一版本問題另外一個go.mod需要注意的是,如果有多個package(包含project本身)有使用相同的package,而大家又是用不同版本的此package,golang會遵守MinimalVersionSelection[11]的原則來選擇package版本,以RussCox提出的sample為例,如果你有A,B,C,D,E,F,G不同的package,而其中A用到B,C,B用到D,C也用到D,F,D用到E,F與G互相依賴,如附圖其中用1,1.1,1.2,1.3,1.4等代表不同版本,經過歸納後,會產生一個建構清單最後會使用A1,B1.2,C1.2,D1.4,E1.2,F1.1,G1.1的版本。

這樣是否會導致問題?理論上在SemanticVersioning的規範下,只要不是major版號更動,API不應該有所變化,而使用最新的理應可以work並且效能或正確性高於舊版。

然而在lib還沒上SemanticVersioninggittag前,這不一定適用於我們的專案,目前測試看起來golang會選擇主要(最上層)的go.mod作為版本依據,這還需要持續觀察以及是否會遇到問題。

最理想的做法是以NF為單位,每個NF所使用的lib固定為NFgo.mod所指定的版號,不同的NF可用不同版本的lib。

Major版本升級如果之後要升級major版號,要如何處理?golang官方不建議major版號升級只生go.mod的版號,畢竟理論上major升級應該是會有不相容的API更新[12],而是建議在使用時就importpackage/name/v2的方式更新[13],如果只更新go.mod會在go.sum出現+incompatible的字樣,提醒你有跟新major版號,你的code使用要檢查。

如何減少major版號更新是設計之初便要思考的,可以參考GoBlog: KeepingYourModulesCompatibleTroubleShootinggo.mod不可以在$GOPATH之中。

因為 lib檢查順序,如果package不明原因無法自動獲取不到(ex.privaterepoissue),可以考慮直接到$GOPATH/pkg/mod或$GOPATH/src下面直接做gitclone,或用copyfolder方式把東西丟上去,唯需要注意路徑,如果是pkg/mod需要把folder名稱重新命名為path/same/as/url/name@version-time-commit,並且要把內部的.gitfolder刪掉(不刪掉應該也沒差,不過go自己抓的好像會沒有);如果是$GOPATH/src只要路徑對就好。

Ref:[1]https://www.inside.com.tw/article/5031-google-2-billion-lines-codeand-one-place,https://ithelp.ithome.com.tw/articles/10217534,http://www.gigamonkeys.com/mono-vs-multi/[2]https://zhuanlan.zhihu.com/p/41627929[3]git--no-pagershow--abbrev=12--format="%cd-%h"--date='format-local:%Y%m%d%H%M%S'--quiet[4]https://segmentfault.com/a/1190000021425527?utm_campaign=studygolang.com&utm_medium=studygolang.com&utm_source=studygolang.com[5]https://openhome.cc/Gossip/Go/Module.html[6]https://golang.org/cmd/go/#hdr-Module_downloading_and_verification[7]https://golang.org/cmd/go/#hdr-Module_authentication_failures[8]https://golang.org/cmd/go/#hdr-Module_configuration_for_non_public_modules,https://golang.org/doc/go1.13#modules[9]https://thewebivore.com/using-replace-in-go-mod-to-point-to-your-local-module/,https://stackoverflow.com/questions/62243083/how-to-change-go-modules-path,https://github.com/golang/go/wiki/Modules#when-should-i-use-the-replace-directive[10]https://golang.org/cmd/go/#hdr-Vendor_Directories[11]https://research.swtch.com/vgo-mvs,https://www.ardanlabs.com/blog/2019/12/modules-03-minimal-version-selection.html,,https://about.sourcegraph.com/blog/the-pain-that-minimal-version-selection-solves/[12]https://semver.org/lang/zh-TW/[13]https://medium.com/@elliotchance/major-versions-in-go-modules-explained-ec7c1df3888b,https://blog.golang.org/v2-go-moduleshttps://github.com/golang/go/wiki/Moduleshttps://roberto.selbach.ca/intro-to-go-modules/https://blog.golang.org/using-go-moduleshttps://juejin.im/post/6844903433846145038go intro languagecommentspoweredbyDisqusRelatedGitHub入門Jupyter設定TensorFlow亂記CiscoSwitch設定NagiousCite ×Copy Download



請為這篇文章評分?