Lucas F. Costa Lidské myšlenky o exaktních vědách
Ahoj všichni! Jak jste si možná všimli, Go mě opravdu zajímá, a protože jsem se do tohoto jazyka zamiloval, rád bych o něm psal častěji.
Pokud Go ještě neznáte, opravdu si myslím, že byste se ho měli naučit. Go je úžasný!
Go je poměrně mladý jazyk a co se týče vendoringu a správy závislostí, stále ještě není úplně vyzrálý. Komunita Go však přejímá některé postupy a vytváří nástroje, které tuto poptávku uspokojují. Právě o tom si dnes budeme povídat.
Poznání ekosystému Go
Go se snaží využívat jednoduchost k dosažení složitých úkolů, a proto je jazyk zábavnější a produktivnější. Od počátku jazyka se tým Go rozhodl, že bude používat řetězcové literály, aby syntaxe importu libovolně popisovala, co se importuje.
Toto se píše v jejich vlastních dokumentech:
Výslovným cílem jazyka Go od počátku bylo, aby bylo možné sestavit kód Go pouze pomocí informací, které se nacházejí v samotném zdrojovém kódu, a nebylo nutné psát makefile nebo jednu z mnoha moderních náhrad makefile. Kdyby Go potřebovalo konfigurační soubor, který by vysvětloval, jak sestavit program, pak by Go selhalo.
Pro dosažení tohoto cíle dává tým Go přednost konvencím před konfiguracemi. Pokud jde o správu balíčků, byly přijaty tyto konvence:
- Cesta importu je vždy odvozena známým způsobem z adresy URL zdrojového kódu. Proto vaše importy vypadají následovně:
github.com/author/pkgname
. Tím také získáme automaticky spravovaný jmenný prostor, protože tyto online služby již spravují jedinečné cesty pro každý balíček. - Místo, kam ukládáme balíčky v našem souborovém systému, je odvozeno známým způsobem z cesty importu. Pokud budete hledat místo, kam
go
ukládá stažené balíčky, budete jej moci spojit s adresou URL, ze které byly staženy. - Každý adresář ve stromu zdrojů odpovídá jednomu balíčku. To vám usnadní hledání zdrojových kódů určitých balíčků a pomůže vám to uspořádat kód standardizovaným způsobem. Díky provázání adresářové struktury se strukturou balíčků se nemusíme starat o obojí současně, protože nástroje souborového systému se stávají nástroji pro správu balíčků.
- Balíček je sestaven pouze na základě informací ze zdrojového kódu. To znamená žádné
makefiles
a žádnéconfiguration
a osvobozuje vás to od nutnosti osvojovat si specifické (a pravděpodobně složité) řetězce nástrojů.
Když jsme si to řekli, je snadné pochopit, proč příkaz go get
funguje tak, jak funguje.
Pochopení příkazů nástrojů Go
Než se pustíme do těchto příkazů, pochopíme, co je to „$GOPATH
„.
Proměnná $GOPATH
je proměnná prostředí, která ukazuje na adresář, který lze považovat za adresář pracovního prostoru. V něm budou uloženy vaše zdrojové kódy, zkompilované balíčky a spustitelné binární soubory.
go get
Příkaz go get
je jednoduchý a funguje téměř jako git clone
. Argument, který musíte příkazu go get
předat, je jednoduchá adresa URL úložiště. V tomto příkladu použijeme příkaz: go get https://github.com/golang/oauth2
.
Při spuštění tohoto příkazu go
jednoduše stáhne balíček z vámi zadané adresy URL a vloží jej do adresáře $GOPATH
. Pokud nyní přejdete do adresáře $GOPATH
, uvidíte, že máte adresář src/github.com/golang/oauth2
, který obsahuje zdrojové soubory balíčku, a zkompilovaný balíček v adresáři pkg
(spolu s jeho závislostmi).
Kdykoli spustíte go get
, měli byste mít na paměti, že všechny stažené balíčky budou umístěny do adresáře, který odpovídá adrese URL, kterou jste použili ke stažení.
Má k dispozici také spoustu dalších příznaků, například -u
, který aktualizuje balíček, nebo -insecure
, který umožňuje stahovat balíčky pomocí nezabezpečených schémat, například HTTP. Více o „pokročilém“ použití příkazu go get
si můžete přečíst na tomto odkazu.
Příkaz go get
také podle go help gopath
aktualizuje podmoduly získávaných balíčků.
go install
Při spuštění příkazu go install
ve zdrojovém adresáři balíčku dojde ke kompilaci nejnovější verze tohoto balíčku a všech jeho závislostí v adresáři pkg
.
go build
Příkaz go build je zodpovědný za kompilaci balíčků a jejich závislostí, ale výsledky neinstaluje!
Pochopení vendoringu
Jak jste si mohli všimnout podle způsobu, jakým Go ukládá své závislosti, má tento přístup ke správě závislostí několik problémů.
Především nejsme schopni určit, kterou verzi balíčku potřebujeme, pokud není umístěna v úplně jiném úložišti, jinak go get
vždy načte nejnovější verzi balíčku. To znamená, že pokud někdo provede zlomovou změnu svého balíčku a neumístí ji do jiného repozitáře, vy a váš tým se dostanete do problémů, protože se může stát, že nakonec budete stahovat různé verze stejného balíčku, což pak povede k problémům typu „funguje na mém počítači“.
Dalším velkým problémem je, že vzhledem k tomu, že go get
instaluje balíčky do kořene adresáře src
, nebudete moci mít pro každý ze svých projektů různé verze závislostí. To znamená, že nemůžete mít projekty, které závisejí na různých verzích stejného balíčku, budete muset mít vždy buď jednu, nebo druhou verzi.
Aby se tyto problémy zmírnily, zavedl tým Go od verze 1.5 funkci vendoring
. Tato funkce umožňuje umístit kód závislého balíčku do jeho vlastního adresáře, takže bude moci vždy získat stejné verze pro všechna sestavení.
Řekněme, že máte projekt s názvem awesome-project
, který závisí na popular-package
. Abyste zaručili, že všichni ve vašem týmu budou používat stejnou verzi popular-package
, můžete jeho zdrojové kódy umístit dovnitř $GOPATH/src/awesome-project/vendor/popular-package
. To bude fungovat, protože Go
se pak pokusí vyřešit cestu k vašim závislostem od adresáře vendor
své vlastní složky (pokud obsahuje alespoň jeden soubor .go
) namísto $GOPATH/src
. Díky tomu budou vaše sestavení také deterministická (reprodukovatelná), protože budou vždy používat stejnou verzi popular-package
.
Je také důležité si všimnout, že příkaz go get
neumístí stažené balíčky automaticky do adresáře vendor
. To je práce pro nástroje vendoringu.
Při použití vendoringu budete moci používat stejné importní cesty, jako kdybyste je nepoužívali, protože Go
se vždy pokusí najít závislosti v nejbližším adresáři vendor
. Není třeba předřazovat vendor/
k žádné z vašich importních cest.
Abychom byli schopni plně pochopit, jak funguje vendoring, musíme porozumět algoritmu, který Go používá k řešení importních cest a který je následující:
- Pátráme po importu v místním adresáři
vendor
(pokud existuje) - Pokud tento balíček nenajdeme v místním adresáři
vendor
, přejdeme do nadřazeného adresáře a pokusíme se ho najít v adresářivendor
tam (jestliže nějaký) - Krok 2 opakujeme, dokud nedojdeme do adresáře $GOPATH/src
- Pohledáme importovaný balíček v adresáři $GOROOT
- Pokud tento balíček nenajdeme v adresáři $GOROOT, hledáme ho v našem adresáři $GOPATH/src
Zjednodušeně řečeno, to znamená, že každý balík může mít své vlastní závislosti přeložené do vlastního adresáře dodavatele. Pokud například závisíte na balíčku x
a balíčku y
a balíček x
závisí také na balíčku y
, ale potřebuje jeho jinou verzi, budete moci svůj kód bez problémů spustit, protože x
bude hledat y
ve své vlastní složce vendor
, zatímco váš balíček bude hledat y
ve složce vendor vašeho projektu.
Nyní praktický příklad. Řekněme, že máte tuto strukturu složek:
$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
Pokud jsme importovali github.com/user/package-one
zevnitř main.go, přeložilo by se to na verzi tohoto balíčku v adresáři vendor
ve stejném adresáři:
$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
Pokud bychom nyní importovali stejný balíček v client.go
, přeloží se tento balíček také do adresáře vendor
ve vlastním adresáři:
$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
To samé se stane při importu tohoto balíčku na soubor server.go
:
$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
Pochopení správy závislostí
Teď, když jsme se dozvěděli všechny tyto věci o tom, jak Go
zpracovává importy a vendory, je nejvyšší čas, abychom si konečně řekli něco o správě závislostí.
Nástroj, který v současné době používám ke správě závislostí ve svých vlastních projektech, se jmenuje godep
. Zdá se, že je velmi oblíbený a funguje mi dobře, proto vám ho vřele doporučuji používat také.
Je postaven na způsobu, jakým funguje vendoring
. Jediné, co musíte udělat, abyste ho mohli začít používat, je použít příkaz godep save
, kdykoli chcete uložit své závislosti do složky vendor
.
Když spustíte godep save
, godep
uloží seznam vašich aktuálních závislostí do složky Godeps/Godeps.json
a poté zkopíruje jejich zdrojový kód do složky vendor
. Je také důležité si uvědomit, že tyto závislosti musíte mít na svém počítači nainstalované, aby je godep
mohl zkopírovat.
Nyní můžete složku vendor
a její obsah odevzdat, abyste zajistili, že při každém spuštění vašeho balíčku budou mít všichni stejné verze stejných balíčků.
Dalším zajímavým příkazem je godep restore
, který nainstaluje verze uvedené v souboru Godeps/Godeps.json
do adresáře $GOPATH
.
Pro aktualizaci závislosti stačí provést aktualizaci pomocí go get -u
(jak jsme o tom mluvili dříve) a poté spustit godep save
, aby godep
aktualizoval soubor Godeps/Godeps.json
a zkopíroval potřebné soubory do adresáře vendor
.
Několik myšlenek o způsobu, jakým Go zpracovává závislosti
Na závěr tohoto příspěvku bych rád přidal také svůj vlastní názor na způsob, jakým Go zpracovává závislosti.
Myslím si, že volba Go používat externí repozitáře pro zpracování jmenných prostorů balíčků byla skvělá, protože spojením koncepce souborového systému s koncepcí jmenných prostorů výrazně zjednodušuje celé řešení balíčků. Díky tomu také celý ekosystém funguje nezávisle, protože nyní máme decentralizovaný způsob získávání balíčků.
Decentralizovaná správa balíčků má však svou cenu, kterou je nemožnost kontrolovat všechny uzly, které jsou součástí této „sítě“. Pokud se například někdo rozhodne, že svůj balíček vyřadí z github
, mohou najednou začít selhávat stovky, tisíce nebo dokonce miliony sestavení. Stejný efekt by mohla mít i změna názvu.
Vzhledem k hlavním cílům Go to dává naprostý smysl a je to naprosto férový kompromis. Go je především o konvencích namísto konfigurace a neexistuje jednodušší způsob, jak pracovat se závislostmi, než jakým to dělá v současnosti.
Jistě, bylo by možné provést několik vylepšení, například používat značky git pro načítání konkrétních verzí balíčků a umožnit uživatelům určit, které verze budou jejich balíčky používat. Bylo by také fajn mít možnost načítat tyto závislosti místo jejich zjišťování ve správě zdrojů. To by nám umožnilo vyhnout se špinavým rozdílům obsahujícím pouze změny kódu v adresáři vendor
a celý repozitář by byl čistší a odlehčenější.
Zapojte se!
Pokud máte nějaké pochybnosti, nápady nebo pokud s něčím, co jsem napsal, nesouhlasíte, podělte se se mnou v komentářích níže nebo mě kontaktujte na @thewizardlucas na Twitteru. Rád si poslechnu, co mi chcete říct, a udělám případné opravy, pokud jsem se dopustil nějaké chyby.
Na závěr se určitě podívejte na tuto úžasnou přednášku Wisdoma Omuyi na GopherConu 2016, ve které vysvětluje, jak Go Vendoring funguje, a také poukazuje na některé detaily o jeho vnitřním fungování.
Díky za přečtení!