Lucas F. Costa Menneskelige tanker om eksakte videnskaber

jan 16, 2022
admin

Hej, alle sammen! Som I måske har bemærket, er jeg virkelig interesseret i Go, og da jeg er blevet forelsket i dette sprog, vil jeg gerne skrive om det oftere.

Hvis du ikke kender Go endnu, synes jeg virkelig, at du bør lære det. Go er fantastisk!

Go er et relativt ungt sprog, og når det kommer til vendoring og afhængighedsstyring, er det stadig ikke rigtig modent endnu. Go-fællesskabet har dog vedtaget nogle praksisser og skabt værktøjer for at dække denne efterspørgsel. Det er det, vi vil tale om i dag.

Forståelse af Go-økosystemet

Go sigter mod at bruge enkelhed til at løse komplekse opgaver og dermed gøre sproget sjovere og mere produktivt. Siden sprogets begyndelse valgte Go-holdet, at de ville bruge stringlitteraler til at gøre importsyntaksen vilkårlig for at beskrive, hvad der blev importeret.

Dette er, hvad der står i deres egen dokumentation:

Et eksplicit mål for Go fra begyndelsen var at kunne bygge Go-kode ved kun at bruge de oplysninger, der findes i selve kildekoden, uden at skulle skrive en makefile eller en af de mange moderne erstatninger for makefiles. Hvis Go havde brug for en konfigurationsfil til at forklare, hvordan man bygger sit program, ville Go have fejlet.

For at nå dette mål foretrækker Go-holdet konventioner frem for konfigurationer. Disse er de konventioner, der er vedtaget, når det drejer sig om pakkehåndtering:

  1. Importstien er altid afledt på en kendt måde fra kildekodens URL-adresse. Det er derfor, at dine importer ser ud som: github.com/author/pkgname. Ved at gøre dette får vi også et automatisk administreret namespace, da disse onlinetjenester allerede administrerer unikke stier for hver pakke.
  2. Det sted, hvor vi gemmer pakker i vores filsystem, er afledt på en kendt måde fra importstien. Hvis du søger efter, hvor go gemmer de pakker, du henter, vil du kunne relatere det til den URL, som den blev hentet fra.
  3. Hver mappe i et kildetræ svarer til en enkelt pakke. Dette gør det lettere for dig at finde kildekode for bestemte pakker og hjælper dig med at organisere din kode på en standardiseret måde. Ved at binde mappestrukturen til pakkestrukturen behøver vi ikke at bekymre os om begge dele på samme tid, fordi filsystemværktøjer bliver til pakkehåndteringsværktøjer.
  4. Pakken opbygges kun ved hjælp af oplysninger fra kildekoden. Det betyder ingen makefiles og ingen configuration og fritager dig for at skulle adoptere specifikke (og sandsynligvis komplicerede) værktøjskæder.

Nu da vi har sagt det, er det let at forstå, hvorfor kommandoen go get fungerer, som den gør.

Forståelse af Go-værktøjernes kommandoer

Hvor vi går ind i disse kommandoer, skal vi forstå, hvad “$GOPATH” er.

Den $GOPATH er en miljøvariabel, der peger på en mappe, som kan betragtes som en arbejdspladsmappe. Den vil indeholde dine kildekoder, kompilerede pakker og kørbare binære filer.

go get

Kommandoen go get er enkel og fungerer næsten som en git clone. Argumentet, du skal sende til go get, er en simpel URL til et repository. I dette eksempel vil vi bruge kommandoen: go get https://github.com/golang/oauth2.

Når du kører denne kommando, henter go simpelthen pakken fra den URL, du har angivet, og lægger den i din $GOPATH-mappe. Hvis du navigerer til din $GOPATH-mappe, vil du nu se, at du har en mappe i src/github.com/golang/oauth2, som indeholder pakkens kildefiler, og en kompileret pakke i pkg-mappen (sammen med dens afhængigheder).

Når du kører go get skal du huske på, at alle hentede pakker vil blive placeret i en mappe, der svarer til den URL, du brugte til at hente den.

Den har også en masse andre flag til rådighed, såsom -u, der opdaterer en pakke, eller -insecure, der giver dig mulighed for at hente pakker ved hjælp af usikre ordninger såsom HTTP. Du kan læse mere om “avanceret” brug af go get-kommandoen på dette link.

I henhold til go help gopath opdaterer go get-kommandoen også undermoduler til de pakker, du henter.

go install

Når du kører go install i en pakkes kildemappe vil den kompilere den seneste version af den pågældende pakke og alle dens afhængigheder i pkg-mappen.

go build

Go build er ansvarlig for at kompilere pakkerne og deres afhængigheder, men den installerer ikke resultaterne!

Forståelse af vendoring

Som du måske har bemærket på den måde, Go gemmer sine afhængigheder på, har denne tilgang til håndtering af afhængigheder et par problemer.

For det første er vi ikke i stand til at bestemme, hvilken version af en pakke vi har brug for, medmindre den er hostet i et helt andet repository, ellers vil go get altid hente den nyeste version af en pakke. Det betyder, at hvis nogen laver en breaking change til deres pakke og ikke lægger den i et andet repo, vil du og dit team ende i problemer, fordi I kan ende med at hente forskellige versioner af den samme pakke, og det vil så føre til “works on my machine”-problemer.

Et andet stort problem er, at på grund af det faktum, at go get installerer pakker til roden af din src-mappe, vil du ikke kunne have forskellige versioner af dine afhængigheder for hvert af dine projekter. Det betyder, at du ikke kan have projekter, der er afhængige af forskellige versioner af den samme pakke, du skal enten have den ene eller den anden version ad gangen.

For at afhjælpe disse problemer har Go-holdet siden Go 1.5 indført en vendoring-funktion. Denne funktion giver dig mulighed for at placere din afhængigheds kode for en pakke i dens egen mappe, så den altid vil kunne få de samme versioner til alle builds.

Lad os sige, at du har et projekt kaldet awesome-project, som afhænger af popular-package. For at garantere, at alle i dit team vil bruge den samme version af popular-package, kan du lægge dets kildekode inde i $GOPATH/src/awesome-project/vendor/popular-package. Dette vil fungere, fordi Go så vil forsøge at opløse dine afhængigheders sti med udgangspunkt i sin egen mappes vendor-mappe (hvis den har mindst én .go-fil) i stedet for $GOPATH/src. Dette vil også gøre dine builds deterministiske (reproducerbare), da de altid vil bruge den samme version af popular-package.

Det er også vigtigt at bemærke, at kommandoen go get ikke automatisk vil lægge de hentede pakker i mappen vendor. Dette er en opgave for vendoring-værktøjer.

Når du bruger vendoring, vil du kunne bruge de samme importstier, som hvis du ikke gjorde det, fordi Go altid vil forsøge at finde afhængigheder i den nærmeste vendor-mappe. Der er ingen grund til at sætte vendor/ foran nogen af dine importstier.

For at kunne forstå fuldt ud, hvordan vendoring fungerer, skal vi forstå den algoritme, som Go bruger til at opløse importstier, og som er følgende:

  1. Læs efter importen i den lokale vendor-mappe (hvis der er nogen)
  2. Hvis vi ikke kan finde denne pakke i den lokale vendor-mappe, går vi op i den overordnede mappe og forsøger at finde den i vendor-mappen der (hvis eventuelle)
  3. Vi gentager trin 2, indtil vi når $GOPATH/src
  4. Vi leder efter den importerede pakke i $GOROOT
  5. Hvis vi ikke kan finde denne pakke i $GOROOT, leder vi efter den i vores mappe $GOPATH/src

Grundlæggende set, betyder det, at hver pakke kan have sine egne afhængigheder opløst til sin egen leverandørmappe. Hvis du f.eks. er afhængig af pakke x og pakke y, og pakke x også er afhængig af pakke y, men har brug for en anden version af den, vil du stadig kunne køre din kode uden problemer, fordi x vil søge efter y i sin egen vendor-mappe, mens din pakke vil søge efter y i dit projekts vendor-mappe.

Nu et praktisk eksempel. Lad os sige, at du har denne mappestruktur:

$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

Hvis vi importerede github.com/user/package-one inde fra main.go ville den opløse til versionen af denne pakke i mappen vendor i samme mappe:

$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

Nu hvis vi importerer den samme pakke i client.go vil den også opløse denne pakke til mappen vendor i sin egen mappe:

$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

Det samme sker, når vi importerer denne pakke på filen 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

Forståelse af afhængighedshåndtering

Nu da vi har lært alle disse ting om, hvordan Go håndterer import og salg, er det på tide, at vi endelig taler om afhængighedshåndtering.

Det værktøj, som jeg i øjeblikket bruger til at administrere afhængigheder i mine egne projekter, hedder godep. Det ser ud til at være meget populært, og det fungerer fint for mig, så jeg kan varmt anbefale dig at bruge det også.

Det er bygget op omkring den måde vendoring fungerer på. Det eneste du skal gøre for at begynde at bruge den er at bruge kommandoen godep save, når du vil gemme dine afhængigheder i din vendor-mappe.

Når du kører godep save, gemmer godep en liste over dine aktuelle afhængigheder i Godeps/Godeps.json og kopierer derefter deres kildekode til vendor-mappen. Det er også vigtigt at bemærke, at du skal have disse afhængigheder installeret på din maskine, for at godep kan kopiere dem.

Nu kan du commit mappen vendor og dens indhold for at sikre, at alle vil have de samme versioner af de samme pakker, når de kører din pakke.

En anden interessant kommando er godep restore, som vil installere de versioner, der er angivet i din Godeps/Godeps.json-fil til din $GOPATH.

For at opdatere en afhængighed skal du blot opdatere den med go get -u (som vi har talt om tidligere) og derefter køre godep save for at godep skal opdatere Godeps/Godeps.json-filen og kopiere de nødvendige filer til vendor-mappen.

Et par tanker om den måde Go håndterer afhængigheder på

I slutningen af dette blogindlæg vil jeg også gerne tilføje min egen mening om den måde Go håndterer afhængigheder på.

Jeg synes, at Go’s valg af at bruge eksterne repositories til at håndtere pakkers namespaces var godt, fordi det gør hele pakkeopløsningen meget mere enkel ved at forene filsystemkoncepterne med namespace-koncepterne. Det får også hele økosystemet til at fungere uafhængigt, fordi vi nu har fået en decentral måde at hente pakker på.

Den decentrale pakkehåndtering har imidlertid en omkostning, nemlig at man ikke kan kontrollere alle de knudepunkter, der er en del af dette “netværk”. Hvis nogen beslutter sig for at tage deres pakke ud af github, for eksempel, så kan hundredvis, tusindvis eller endda millioner af builds pludselig begynde at fejle. Navneændringer kan også have samme effekt.

Med tanke på Go’s hovedmål giver dette fuldstændig mening og er en helt rimelig afvejning. Go handler om konventioner i stedet for konfiguration, og der findes ikke nogen enklere måde at håndtere afhængigheder på end den måde, som den gør i øjeblikket.

Selvfølgelig kunne der laves nogle få forbedringer, såsom at bruge git-tags til at hente specifikke versioner af pakker og give brugerne mulighed for at angive, hvilke versioner deres pakker skal bruge. Det ville også være fedt at kunne hente disse afhængigheder i stedet for at tjekke dem ud på kildekontrol. Det ville gøre det muligt for os at undgå beskidte diffs, der kun indeholder ændringer til kode i vendor-mappen, og gøre hele repositoriet renere og mere letvægtigt.

Get in touch!

Hvis du er i tvivl, har tanker eller er uenig i noget af det, jeg har skrevet, så del det med mig i kommentarerne nedenfor eller kontakt mig på @thewizardlucas på twitter. Jeg vil elske at høre hvad du har at sige og lave rettelser, hvis jeg har lavet fejl.

Sidst, men ikke mindst, skal du sørge for at tage et kig på dette fantastiske foredrag af Wisdom Omuya på GopherCon 2016, hvor han forklarer hvordan Go Vendoring fungerer og også påpeger nogle detaljer om det indre arbejde.

Tak for at læse dette!

Skriv et svar

Din e-mailadresse vil ikke blive publiceret.