Lucas F. Costa Emberi gondolatok az egzakt tudományokról
Szia, mindenki! Mint talán észrevettétek, nagyon érdekel a Go, és mivel beleszerettem ebbe a nyelvbe, szeretnék gyakrabban írni róla.
Ha még nem ismered a Go-t, akkor szerintem tényleg meg kellene tanulnod. A Go fantasztikus!
A Go egy viszonylag fiatal nyelv, és amikor a vendorálásról és a függőségkezelésről van szó, még mindig nem igazán érett. A Go közösség azonban átvett néhány gyakorlatot, és eszközöket hozott létre ennek az igénynek a kielégítésére. Erről fogunk ma beszélni.
A Go ökoszisztéma megismerése
A Go célja, hogy az egyszerűség segítségével összetett feladatokat valósítson meg, és ezáltal a nyelvet szórakoztatóbbá és produktívabbá tegye. A nyelv kezdete óta a Go csapat úgy döntött, hogy az import szintaxis önkényessé tételéhez string literálokat használnak, hogy leírják, mit importálnak.
Ezt írják a saját dokumentációjukban:
A Go kifejezett célja a kezdetektől fogva az volt, hogy a Go kódot csak a magában a forrásban található információk felhasználásával lehessen építeni, ne kelljen makefile-t vagy a makefile-ok számos modern helyettesítőjének egyikét írni. Ha a Gónak szüksége lenne egy konfigurációs fájlra ahhoz, hogy elmagyarázza, hogyan építse fel a programját, akkor a Go kudarcot vallott volna.
Egy ilyen cél elérése érdekében a Go csapat a konvenciókat részesíti előnyben a konfigurációkkal szemben. Ezek az elfogadott konvenciók, amikor a csomagkezelésről van szó:
- Az importálási útvonal mindig ismert módon származik a forráskód URL-jéből. Ezért néznek ki az importok így:
github.com/author/pkgname
. Ezzel egy automatikusan kezelt névteret is kapunk, hiszen ezek az online szolgáltatások már eleve egyedi elérési utakat kezelnek minden csomaghoz. - Az import útvonalból ismert módon származtatjuk azt a helyet, ahol a csomagokat a fájlrendszerünkben tároljuk. Ha rákeresünk, hogy
go
hol tárolja a letöltött csomagokat, akkor azt az URL-hez tudjuk kapcsolni, ahonnan letöltöttük. - A forrásfában minden könyvtár egyetlen csomagnak felel meg. Ez megkönnyíti az egyes csomagok forráskódjának megtalálását, és segít szabványosított módon megszervezni a kódot. Azzal, hogy a mappaszerkezetet a csomagszerkezethez kötjük, nem kell egyszerre mindkettővel foglalkoznunk, mert a fájlrendszer eszközei csomagkezelő eszközökké válnak.
- A csomag csak a forráskódból származó információk felhasználásával épül fel. Ez azt jelenti, hogy nincs
makefiles
és nincsconfiguration
, és megszabadít attól, hogy speciális (és valószínűleg bonyolult) eszközláncokat kelljen elfogadnunk.
Most, hogy ezt elmondtuk, már könnyű megérteni, hogy a go get
parancs miért működik úgy, ahogyan működik.
A Go Tools parancsainak megértése
Mielőtt belemennénk ezekbe a parancsokba, értsük meg, mi az a “$GOPATH
“.
A $GOPATH
egy környezeti változó, amely egy olyan könyvtárra mutat, amely munkaterület könyvtárnak tekinthető. Ez fogja tárolni a forráskódokat, a lefordított csomagokat és a futtatható binárisokat.
go get
A go get
parancs egyszerű és szinte úgy működik, mint a git clone
. A go get
-nek átadandó argumentum egy egyszerű tároló URL címe. Ebben a példában a parancsot fogjuk használni: go get https://github.com/golang/oauth2
.
A parancs futtatásakor a go
egyszerűen lehívja a csomagot a megadott URL-ről, és a $GOPATH
könyvtárba helyezi. Ha a $GOPATH
mappába navigálsz, akkor most látni fogod, hogy van egy mappád a src/github.com/golang/oauth2
könyvtárban, amely tartalmazza a csomag forrásfájljait, és egy lefordított csomag a pkg
könyvtárban (a függőségekkel együtt).
Az go get
futtatásakor szem előtt kell tartanod, hogy a letöltött csomagok a letöltéshez használt URL-nek megfelelő könyvtárba kerülnek.
Egy csomó más flag is rendelkezésre áll, például a -u
, amely frissíti a csomagot, vagy a -insecure
, amely lehetővé teszi a csomagok letöltését nem biztonságos sémák, például HTTP segítségével. A go get
parancs “haladó” használatáról ezen a linken olvashatsz többet.
Az go help gopath
szerint a go get
parancs a megszerzett csomagok almoduljait is frissíti.
go install
Az go install
parancsot egy csomag forráskönyvtárában futtatva lefordítja az adott csomag legújabb verzióját és az összes függőségét a pkg
könyvtárba.
go build
Go build a csomagok és függőségeik lefordításáért felelős, de az eredményeket nem telepíti!
A függőségkezelés megértése
Amint azt már észrevehettük abból, ahogyan a Go menti a függőségeit, a függőségkezelésnek ez a megközelítése néhány problémával jár.
Először is, nem tudjuk meghatározni, hogy egy csomag melyik verziójára van szükségünk, hacsak nem egy teljesen más tárolóban van elhelyezve, különben a go get
mindig a csomag legújabb verzióját fogja lekérni. Ez azt jelenti, hogy ha valaki egy törő változtatást végez a csomagján, és nem teszi azt egy másik repóba, akkor te és a csapatod bajba kerülhet, mert a végén ugyanannak a csomagnak különböző verzióit fogod lehívni, ami aztán “az én gépemen működik” problémákhoz fog vezetni.
Egy másik nagy probléma, hogy mivel a go get
a csomagokat a src
könyvtár gyökerébe telepíti, nem leszel képes arra, hogy a függőségek különböző verzióit használd minden egyes projektedhez. Ez azt jelenti, hogy nem lehetnek olyan projektjeid, amelyek ugyanannak a csomagnak különböző verzióitól függenek, vagy az egyik, vagy a másik változatot kell egyszerre használnod.
Azért, hogy ezeket a problémákat enyhítsék, a Go 1.5 óta a Go csapat bevezetett egy vendoring
funkciót. Ez a funkció lehetővé teszi, hogy a függőségi csomagod kódját a saját könyvtárába helyezd, így az mindig ugyanazokat a verziókat kapja minden buildhez.
Tegyük fel, hogy van egy awesome-project
nevű projekted, ami függ a popular-package
-től. Annak érdekében, hogy garantáld, hogy a csapatodban mindenki ugyanazt a verziót használja a popular-package
-ből, a forrását a $GOPATH/src/awesome-project/vendor/popular-package
-ban helyezheted el. Ez azért fog működni, mert a Go
ekkor a $GOPATH/src
helyett a saját mappájának vendor
könyvtárából kiindulva próbálja majd feloldani a függőségek elérési útvonalát (ha van legalább egy .go
fájl). Ez determinisztikussá (reprodukálhatóvá) teszi a buildjeidet is, mivel mindig ugyanazt a popular-package
verziót fogják használni.
Azt is fontos megjegyezni, hogy a go get
parancs nem fogja automatikusan a vendor
mappába tenni a letöltött csomagokat. Ez a vendoring eszközök feladata.
A vendoring használata esetén ugyanazokat az importálási útvonalakat használhatjuk, mintha nem használnánk, mert a Go
mindig a legközelebbi vendor
könyvtárban próbálja megtalálni a függőségeket. Nincs szükség arra, hogy az importálási útvonalak elé vendor/
-ot írj.
Hogy teljesen megértsük, hogyan működik a vendorálás, meg kell értenünk a Go által az importálási útvonalak feloldására használt algoritmust, ami a következő:
- Keresd meg az importot a helyi
vendor
könyvtárban (ha van) - Ha nem találjuk ezt a csomagot a helyi
vendor
könyvtárban, akkor felmegyünk a szülői mappába, és megpróbáljuk megtalálni az ottanivendor
könyvtárban (ha van) - Megismételjük a 2. lépést, amíg el nem érjük a $GOPATH/src
- Az importált csomagot a $GOROOT-ban keressük
- Ha nem találjuk ezt a csomagot a $GOROOT-ban, akkor a $GOPATH/src mappánkban keressük
Gyakorlatilag, ez azt jelenti, hogy minden csomagnak saját függőségeit a saját szállítói könyvtárában oldhatjuk fel. Ha például függsz a x
és a y
csomagtól, és a x
csomag szintén függ a y
csomagtól, de annak egy másik verziójára van szüksége, akkor is gond nélkül futtathatod a kódodat, mert a x
a saját vendor
mappájában keresi a y
-et, míg a te csomagod a projekted vendor mappájában keresi a y
-et.
Most, egy gyakorlati példa. Tegyük fel, hogy a következő mappaszerkezeted van:
$GOPATH src/ github.com/user/package-one/ one.go myproject main.go vendor/ github.com/user/package-one/ one.go client/ client.go vendor/ github.com/user/package-one/ server/ server.go vendor/ github.com/user/package-one/ one.go
Ha a main-on belülről importáltuk a github.com/user/package-one
.go, akkor ennek a csomagnak a vendor
könyvtárban lévő változatára oldódna fel ugyanabban a mappában:
$GOPATH src/ github.com/user/package-one/ one.go myproject main.go <-- Importing package-one from here vendor/ github.com/user/package-one/ <-- resolves to here one.go client/ client.go vendor/ github.com/user/package-one/ server/ server.go vendor/ github.com/user/package-one/ one.go
Most ha ugyanezt a csomagot a client.go
-ben importáljuk, akkor ezt a csomagot is a vendor
mappába fogja feloldani a saját könyvtárában:
$GOPATH src/ github.com/user/package-one/ one.go myproject main.go vendor/ github.com/user/package-one/ one.go client/ client.go <-- Importing package-one from here vendor/ github.com/user/package-one/ <-- resolves to here server/ server.go vendor/ github.com/user/package-one/ one.go
Ugyanez történik, ha a server.go
mappában lévő csomagot importáljuk:
$GOPATH src/ github.com/user/package-one/ one.go myproject main.go vendor/ github.com/user/package-one/ one.go client/ client.go vendor/ github.com/user/package-one/ server/ server.go <-- Importing package-one from here vendor/ github.com/user/package-one/ <-- resolves to here one.go
A függőségkezelés megértése
Most, hogy mindezt megtanultuk arról, hogyan kezeli a Go
az importálást és a vendorálást, itt az ideje, hogy végre a függőségkezelésről beszéljünk.
Az eszköz, amit jelenleg a függőségek kezelésére használok a saját projektjeimben, a godep
. Úgy tűnik, hogy nagyon népszerű, és nekem jól működik, ezért nagyon ajánlom, hogy te is ezt használd.
Ez a vendoring
működésére épül. Mindössze annyit kell tenned, hogy elkezdd használni, hogy használd a godep save
parancsot, amikor el akarod menteni a függőségeidet a vendor
mappádba.
Amikor futtatod a godep save
parancsot, a godep
elmenti az aktuális függőségeid listáját a Godeps/Godeps.json
mappába, majd átmásolja a forráskódjukat a vendor
mappába. Azt is fontos megjegyezni, hogy ezeket a függőségeket telepíteni kell a gépedre ahhoz, hogy a godep
képes legyen bemásolni őket.
Most már rögzítheted a vendor
mappát és annak tartalmát, hogy mindenki ugyanazoknak a csomagoknak ugyanazokat a verzióit kapja, amikor a csomagodat futtatod.
Egy másik érdekes parancs a godep restore
, amely a Godeps/Godeps.json
fájlban megadott verziókat telepíti a $GOPATH
könyvtárba.
A függőség frissítéséhez csak annyit kell tenned, hogy frissíted a go get -u
segítségével (ahogy korábban már beszéltünk róla), majd futtasd a godep save
parancsot, hogy a godep
frissítse a Godeps/Godeps.json
fájlt és másolja a szükséges fájlokat a vendor
könyvtárba.
Pár gondolat arról, ahogy a Go kezeli a függőségeket
A blogbejegyzés végén szeretném hozzáfűzni a saját véleményemet is arról, ahogy a Go kezeli a függőségeket.
Úgy gondolom, hogy a Go döntése, hogy külső tárolókat használ a csomagok névtereinek kezelésére, nagyszerű volt, mert sokkal egyszerűbbé teszi az egész csomagfeloldást azáltal, hogy a fájlrendszer és a névtér fogalmakat összekapcsolja. Ez az egész ökoszisztémát is függetlenné teszi, mert most már van egy decentralizált módja a csomagok lehívásának.
A decentralizált csomagkezelésnek azonban ára van, mégpedig az, hogy nem tudjuk ellenőrizni az összes csomópontot, amelyek ennek a “hálózatnak” a részét képezik. Ha valaki például úgy dönt, hogy kiveszi a csomagját a github
-ről, akkor hirtelen több száz, ezer vagy akár több millió build elkezdhet hibásan működni. A névváltoztatások is ugyanezt a hatást érhetik el.
A Go fő céljait figyelembe véve ennek teljesen értelme van, és ez egy teljesen korrekt kompromisszum. A Go a konfiguráció helyett a konvenciókról szól, és nincs egyszerűbb módja a függőségek kezelésének, mint ahogyan azt jelenleg teszi.
Azért persze lehetne néhány fejlesztést eszközölni, például a git-tagek használatával konkrét csomagverziókat lehetne lekérni, és a felhasználóknak meg lehetne adni, hogy milyen verziókat használjanak a csomagjaik. Az is klassz lenne, ha ezeket a függőségeket le lehetne hívni ahelyett, hogy a forráskezelésen ellenőriznénk őket. Ez lehetővé tenné, hogy elkerüljük a piszkos diffeket, amelyek csak a vendor
könyvtárban lévő kód módosításait tartalmazzák, és tisztábbá és könnyebbé tenné az egész tárat.
Kapcsolat!
Ha kétségeid, gondolataid vannak, vagy ha nem értesz egyet azzal, amit írtam, kérlek, oszd meg velem az alábbi hozzászólásokban, vagy keress meg a @thewizardlucas Twitteren. Szívesen meghallgatnám a véleményedet, és javítanék, ha hibáztam.
Végül, de nem utolsósorban, mindenképpen nézd meg Wisdom Omuya fantasztikus előadását a GopherCon 2016-on, amelyben elmagyarázza, hogyan működik a Go Vendoring, és rámutat néhány részletre a belső működésével kapcsolatban is.
Köszönöm, hogy elolvastad ezt!