Lucas F. Costa Pensieri umani sulle scienze esatte
Ciao a tutti! Come avrete notato sono molto interessato a Go e da quando mi sono innamorato di questo linguaggio mi piacerebbe scriverne più frequentemente.
Se non conoscete ancora Go penso proprio che dovreste impararlo. Go è fantastico!
Go è un linguaggio relativamente giovane e quando si tratta di vendoring e gestione delle dipendenze non è ancora molto maturo. La comunità di Go, tuttavia, sta adottando alcune pratiche e creando strumenti per soddisfare questa domanda. Questo è ciò di cui parleremo oggi.
Comprendere l’ecosistema di Go
Go mira ad usare la semplicità per realizzare compiti complessi e quindi rendere il linguaggio più divertente e produttivo. Fin dall’inizio del linguaggio, il team di Go ha scelto di usare i letterali di stringa per rendere la sintassi di importazione arbitraria per descrivere ciò che veniva importato.
Questo è ciò che è scritto nei loro stessi documenti:
Un obiettivo esplicito per Go fin dall’inizio era quello di essere in grado di costruire il codice Go usando solo le informazioni trovate nel sorgente stesso, senza bisogno di scrivere un makefile o uno dei molti sostituti moderni per i makefile. Se Go avesse avuto bisogno di un file di configurazione per spiegare come costruire il programma, allora Go avrebbe fallito.
Per raggiungere questo obiettivo, il team di Go preferisce le convenzioni alle configurazioni. Queste sono le convenzioni adottate quando si tratta della gestione dei pacchetti:
- Il percorso di importazione è sempre derivato in modo noto dall’URL del codice sorgente. Questo è il motivo per cui le tue importazioni appaiono come:
github.com/author/pkgname
. Facendo questo otteniamo anche uno spazio dei nomi gestito automaticamente, poiché questi servizi online gestiscono già percorsi unici per ogni pacchetto. - Il luogo in cui memorizziamo i pacchetti nel nostro file system è derivato in modo noto dal percorso di importazione. Se cerchi dove
go
memorizza i pacchetti che scarichi, sarai in grado di metterlo in relazione con l’URL da cui è stato scaricato. - Ogni directory in un albero sorgente corrisponde a un singolo pacchetto. Questo rende più facile per voi trovare il codice sorgente per certi pacchetti e vi aiuta ad organizzare il vostro codice in modo standardizzato. Legando la struttura delle cartelle a quella dei pacchetti non abbiamo bisogno di preoccuparci di entrambe contemporaneamente, perché gli strumenti del file system diventano strumenti di gestione dei pacchetti.
- Il pacchetto è costruito usando solo informazioni dal codice sorgente. Questo significa niente
makefiles
e nienteconfiguration
e vi libera dal dover adottare specifiche (e probabilmente complicate) catene di strumenti.
Ora che abbiamo detto questo, è facile capire perché il comando go get
funziona come funziona.
Comprensione dei comandi di Go Tools
Prima di addentrarci in questi comandi cerchiamo di capire cos’è il “$GOPATH
“.
Il $GOPATH
è una variabile d’ambiente che punta a una directory che può essere considerata come una directory dello spazio di lavoro. Conterrà i tuoi codici sorgente, i pacchetti compilati e i binari eseguibili.
go get
Il comando go get
è semplice e funziona quasi come un git clone
. L’argomento che devi passare a go get
è un semplice URL del repository. In questo esempio, useremo il comando go get https://github.com/golang/oauth2
.
Quando si esegue questo comando, go
semplicemente recupera il pacchetto dall’URL che hai fornito e lo mette nella tua directory $GOPATH
. Se navighi nella tua cartella $GOPATH
vedrai che ora hai una cartella in src/github.com/golang/oauth2
che contiene i file sorgente del pacchetto e un pacchetto compilato nella directory pkg
(insieme alle sue dipendenze).
Ogni volta che si esegue go get
si dovrebbe avere in mente che qualsiasi pacchetto scaricato sarà posto in una directory che corrisponde all’URL utilizzato per scaricarlo.
Ha anche un mucchio di altri flag disponibili, come -u
che aggiorna un pacchetto o -insecure
che permette di scaricare pacchetti utilizzando schemi insicuri come HTTP. Puoi leggere di più sull’uso “avanzato” del comando go get
a questo link.
Inoltre, secondo go help gopath
, il comando go get
aggiorna anche i sottomoduli dei pacchetti che stai ricevendo.
go install
Quando si esegue go install
nella directory dei sorgenti di un pacchetto si compila l’ultima versione di quel pacchetto e tutte le sue dipendenze nella directory pkg
.
go build
Go build è responsabile della compilazione dei pacchetti e delle loro dipendenze, ma non installa i risultati!
Comprensione del Vendoring
Come avrete notato dal modo in cui Go salva le sue dipendenze, questo approccio alla gestione delle dipendenze ha alcuni problemi.
Prima di tutto, non siamo in grado di determinare quale versione di un pacchetto ci serve a meno che non sia ospitata in un repository completamente diverso, altrimenti go get
recupererà sempre l’ultima versione di un pacchetto. Questo significa che se qualcuno fa un cambiamento radicale al suo pacchetto e non lo mette in un altro repo tu e il tuo team finirete nei guai perché potreste finire per recuperare versioni diverse dello stesso pacchetto e questo porterà a problemi di “funziona sulla mia macchina”.
Un altro grosso problema è che, a causa del fatto che go get
installa i pacchetti nella root della tua directory src
, non potrai avere versioni diverse delle tue dipendenze per ognuno dei tuoi progetti. Questo significa che non puoi avere progetti che dipendono da versioni diverse dello stesso pacchetto, dovrai avere una versione o l’altra alla volta.
Per mitigare questi problemi, da Go 1.5, il team di Go ha introdotto una caratteristica vendoring
. Questa caratteristica ti permette di mettere il codice della tua dipendenza per un pacchetto all’interno della sua propria directory in modo che possa ottenere sempre le stesse versioni per tutte le build.
Diciamo che hai un progetto chiamato awesome-project
che dipende da popular-package
. Per garantire che tutti nel vostro team usino la stessa versione di popular-package
potete mettere il suo sorgente dentro $GOPATH/src/awesome-project/vendor/popular-package
. Questo funzionerà perché Go
cercherà di risolvere il percorso delle vostre dipendenze partendo dalla directory vendor
della sua stessa cartella (se ha almeno un file .go
) invece di $GOPATH/src
. Questo renderà anche le vostre compilazioni deterministiche (riproducibili) poiché useranno sempre la stessa versione di popular-package
.
È anche importante notare che il comando go get
non metterà automaticamente i pacchetti scaricati nella cartella vendor
. Questo è un lavoro per gli strumenti di vendoring.
Quando si usa il vendoring si potranno usare gli stessi percorsi di importazione come se non lo si facesse, perché Go
cercherà sempre di trovare le dipendenze nella directory vendor
più vicina. Non c’è bisogno di anteporre vendor/
a nessuno dei tuoi percorsi di importazione.
Per poter comprendere appieno come funziona il vendoring dobbiamo capire l’algoritmo usato da Go per risolvere i percorsi di importazione, che è il seguente:
- Cercare l’importazione nella directory locale
vendor
(se esiste) - Se non riusciamo a trovare questo pacchetto nella directory locale
vendor
saliamo nella cartella padre e proviamo a trovarlo nella directoryvendor
(se - Ripetiamo il passo 2 fino a raggiungere $GOPATH/src
- Cerchiamo il pacchetto importato in $GOROOT
- Se non troviamo questo pacchetto in $GOROOT lo cerchiamo nella nostra cartella $GOPATH/src
In pratica, questo significa che ogni pacchetto può avere le proprie dipendenze risolte nella propria cartella vendor. Se dipendete dal pacchetto x
e dal pacchetto y
, per esempio, e anche il pacchetto x
dipende dal pacchetto y
ma ha bisogno di una versione diversa di esso, sarete ancora in grado di eseguire il vostro codice senza problemi, perché x
cercherà y
nella sua cartella vendor
mentre il vostro pacchetto cercherà y
nella cartella vendor del vostro progetto.
Ora, un esempio pratico. Diciamo che hai questa struttura di cartelle:
$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
Se importassimo github.com/user/package-one
da dentro main.go si risolverebbe alla versione di questo pacchetto nella directory vendor
nella stessa cartella:
$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
Ora se importiamo lo stesso pacchetto in client.go
si risolverà anche questo pacchetto nella cartella vendor
nella propria directory:
$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
Lo stesso accade quando si importa questo pacchetto nel file 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
Comprensione della gestione delle dipendenze
Ora che abbiamo imparato tutte queste cose su come Go
gestisce le importazioni e le vendite, è ora di parlare della gestione delle dipendenze.
Lo strumento che sto attualmente usando per gestire le dipendenze nei miei progetti si chiama godep
. Sembra essere molto popolare e funziona bene per me, quindi vi consiglio vivamente di usare anche quello.
È costruito intorno al modo in cui funziona vendoring
. Tutto quello che devi fare per iniziare ad usarlo è usare il comando godep save
ogni volta che vuoi salvare le tue dipendenze nella tua cartella vendor
.
Quando esegui godep save
, godep
salverà una lista delle tue attuali dipendenze in Godeps/Godeps.json
e poi copierà il loro codice sorgente nella cartella vendor
. È anche importante notare che devi avere queste dipendenze installate sulla tua macchina affinché godep
sia in grado di copiarle.
Ora puoi fare il commit della cartella vendor
e del suo contenuto per assicurarti che tutti abbiano le stesse versioni degli stessi pacchetti quando eseguono il tuo pacchetto.
Un altro comando interessante è godep restore
, che installerà le versioni specificate nel tuo file Godeps/Godeps.json
nel tuo $GOPATH
.
Per aggiornare una dipendenza tutto quello che devi fare è aggiornarla usando go get -u
(come abbiamo detto prima) e poi eseguire godep save
in modo che godep
aggiorni il file Godeps/Godeps.json
e copi i file necessari nella cartella vendor
.
Poche riflessioni sul modo in cui Go gestisce le dipendenze
Alla fine di questo post, vorrei anche aggiungere la mia opinione sul modo in cui Go gestisce le dipendenze.
Penso che la scelta di Go di usare repository esterni per gestire gli spazi dei nomi dei pacchetti sia ottima perché rende l’intera risoluzione dei pacchetti molto più semplice unendo i concetti di file system a quelli di spazio dei nomi. Questo fa anche funzionare l’intero ecosistema in modo indipendente perché ora abbiamo un modo decentralizzato per recuperare i pacchetti.
Tuttavia, la gestione decentralizzata dei pacchetti ha un costo, che è quello di non essere in grado di controllare tutti i nodi che fanno parte di questa “rete”. Se qualcuno decide di togliere il suo pacchetto da github
, per esempio, allora centinaia, migliaia o addirittura milioni di build possono iniziare a fallire all’improvviso. Anche i cambiamenti di nome potrebbero avere lo stesso effetto.
Considerando gli obiettivi principali di Go, questo ha completamente senso ed è un compromesso assolutamente giusto. Go è tutto basato sulla convenzione invece che sulla configurazione e non c’è modo più semplice di gestire le dipendenze di quello attuale.
Certo, si potrebbero fare alcuni miglioramenti, come usare i tag git per recuperare versioni specifiche dei pacchetti e permettere agli utenti di specificare quali versioni useranno i loro pacchetti. Sarebbe anche bello essere in grado di recuperare queste dipendenze invece di controllarle sul controllo dei sorgenti. Questo ci permetterebbe di evitare diff sporchi contenenti solo modifiche al codice nella directory vendor
e renderebbe l’intero repository più pulito e leggero.
Mettiti in contatto!
Se hai dubbi, pensieri o se non sei d’accordo con qualcosa che ho scritto, condividilo con me nei commenti qui sotto o raggiungimi a @thewizardlucas su twitter. Mi piacerebbe sentire cosa avete da dire e fare eventuali correzioni se ho fatto qualche errore.
Infine, ma non meno importante, assicuratevi di dare un’occhiata a questo fantastico discorso di Wisdom Omuya su GopherCon 2016 in cui spiega come funziona Go Vendoring e sottolinea anche alcuni dettagli sul suo funzionamento interno.
Grazie per aver letto questo!