Lucas F. Costa Pensamientos humanos sobre ciencias exactas
¡Hola a todos! Como habréis notado estoy muy interesado en Go y desde que me enamoré de este lenguaje me gustaría escribir sobre él con más frecuencia.
Si aún no conocéis Go creo que deberíais aprenderlo.
Go es un lenguaje relativamente joven y cuando se trata de la venta y la gestión de la dependencia todavía no está realmente maduro. La comunidad de Go, sin embargo, ha ido adoptando algunas prácticas y creando herramientas para suplir esta demanda. De esto es de lo que vamos a hablar hoy.
Entendiendo el ecosistema de Go
Go pretende utilizar la simplicidad para conseguir tareas complejas y, por tanto, hacer el lenguaje más divertido y productivo. Desde el principio del lenguaje, el equipo de Go eligió que utilizaría literales de cadena para hacer la sintaxis de importación arbitraria para describir lo que se estaba importando.
Esto es lo que está escrito en sus propios docs:
Un objetivo explícito para Go desde el principio era ser capaz de construir código Go utilizando sólo la información que se encuentra en el propio código fuente, sin necesidad de escribir un makefile o uno de los muchos reemplazos modernos para makefiles. Si Go necesitara un archivo de configuración para explicar cómo construir su programa, entonces Go habría fracasado.
Para lograr este objetivo, el equipo de Go favorece las convenciones sobre las configuraciones. Estas son las convenciones adoptadas cuando se trata de la gestión de paquetes:
- La ruta de importación siempre se deriva de forma conocida de la URL del código fuente. Por eso sus importaciones tienen el aspecto de:
github.com/author/pkgname
. Al hacer esto también conseguimos un espacio de nombres gestionado automáticamente, ya que estos servicios online ya gestionan rutas únicas para cada paquete. - El lugar donde almacenamos los paquetes en nuestro sistema de archivos se deriva de forma conocida de la ruta de importación. Si buscas dónde
go
almacena los paquetes que descargas podrás relacionarlo con la URL de la que se descargó. - Cada directorio de un árbol de fuentes se corresponde con un único paquete. Esto le facilita la búsqueda del código fuente de determinados paquetes y le ayuda a organizar su código de forma estandarizada. Al vincular la estructura de carpetas a la estructura de paquetes no tenemos que preocuparnos de ambas al mismo tiempo, ya que las herramientas del sistema de archivos se convierten en herramientas de gestión de paquetes.
- El paquete se construye utilizando únicamente la información del código fuente. Esto significa que no hay
makefiles
niconfiguration
y le libera de tener que adoptar cadenas de herramientas específicas (y probablemente complicadas).
Ahora que hemos dicho eso, es fácil entender por qué el comando go get
funciona como lo hace.
Entendiendo los comandos de las herramientas de Go
Antes de entrar en estos comandos vamos a entender qué es el «$GOPATH
«.
El $GOPATH
es una variable de entorno que apunta a un directorio que puede ser considerado como un directorio de espacio de trabajo. En él se guardarán los códigos fuente, los paquetes compilados y los binarios ejecutables.
go get
El comando go get
es sencillo y funciona casi como un git clone
. El argumento que debes pasar a go get
es una simple URL de repositorio. En este ejemplo, utilizaremos el comando: go get https://github.com/golang/oauth2
.
Cuando se ejecuta este comando, go
simplemente obtendrá el paquete de la URL que proporcionaste y lo pondrá en tu directorio $GOPATH
. Si navegas a tu carpeta $GOPATH
verás que ahora tienes una carpeta en src/github.com/golang/oauth2
que contiene los archivos fuente del paquete y un paquete compilado en el directorio pkg
(junto con sus dependencias).
Cuando ejecutes go get
debes tener en cuenta que cualquier paquete descargado se colocará en un directorio que corresponde a la URL que usaste para descargarlo.
También tiene un montón de otras banderas disponibles, como -u
que actualiza un paquete o -insecure
que te permite descargar paquetes usando esquemas inseguros como HTTP. Puedes leer más sobre el uso «avanzado» del comando go get
en este enlace.
Además, según go help gopath
, el comando go get
también actualiza los submódulos de los paquetes que estás obteniendo.
go install
Cuando se ejecuta go install
en el directorio fuente de un paquete se compila la última versión de ese paquete y todas sus dependencias en el directorio pkg
.
go build
Go build se encarga de compilar los paquetes y sus dependencias, ¡pero no instala los resultados!
Entendiendo el Vendoring
Como te habrás dado cuenta por la forma en que Go guarda sus dependencias, esta aproximación a la gestión de dependencias tiene algunos problemas.
En primer lugar, no somos capaces de determinar qué versión de un paquete necesitamos a no ser que esté alojada en un repositorio completamente diferente, de lo contrario go get
obtendrá siempre la última versión de un paquete. Esto significa que si alguien hace un cambio de ruptura a su paquete y no lo pone en otro repositorio, usted y su equipo terminarán en problemas porque podría terminar obteniendo diferentes versiones del mismo paquete y esto entonces conducirá a problemas de «funciona en mi máquina».
Otro gran problema es que, debido al hecho de que go get
instala los paquetes a la raíz de su directorio src
, usted no será capaz de tener diferentes versiones de sus dependencias para cada uno de sus proyectos. Esto significa que no puedes tener proyectos que dependan de diferentes versiones del mismo paquete, tendrás que tener una versión u otra a la vez.
Para mitigar estos problemas, desde Go 1.5, el equipo de Go introdujo una característica vendoring
. Esta característica le permite poner el código de su dependencia para un paquete dentro de su propio directorio por lo que será capaz de obtener siempre las mismas versiones para todas las construcciones.
Supongamos que usted tiene un proyecto llamado awesome-project
que depende de popular-package
. Para garantizar que todo el mundo en su equipo utilizará la misma versión de popular-package
puede poner su fuente dentro de $GOPATH/src/awesome-project/vendor/popular-package
. Esto funcionará porque Go
intentará resolver la ruta de sus dependencias empezando por el directorio vendor
de su propia carpeta (si tiene al menos un archivo .go
) en lugar de $GOPATH/src
. Esto también hará que sus construcciones sean deterministas (reproducibles) ya que siempre usarán la misma versión de popular-package
.
También es importante notar que el comando go get
no pondrá los paquetes descargados en la carpeta vendor
automáticamente. Este es un trabajo para las herramientas de vendoring.
Cuando se usa vendoring podrá usar las mismas rutas de importación que si no lo hiciera, porque Go
siempre tratará de encontrar dependencias en el directorio vendor
más cercano. No es necesario anteponer vendor/
a ninguna de sus rutas de importación.
Para poder entender bien cómo funciona la venta debemos entender el algoritmo que utiliza Go para resolver las rutas de importación, que es el siguiente:
- Busca la importación en el directorio local
vendor
(si lo hay) - Si no encontramos este paquete en el directorio local
vendor
subimos a la carpeta padre y tratamos de encontrarlo en el directoriovendor
de allí (si alguno) - Repitimos el paso 2 hasta llegar a $GOPATH/src
- Buscamos el paquete importado en $GOROOT
- Si no encontramos este paquete en $GOROOT lo buscamos en nuestra carpeta $GOPATH/src
Básicamente, esto significa que cada paquete puede tener sus propias dependencias resueltas en su propio directorio de proveedores. Si dependes del paquete x
y del paquete y
, por ejemplo, y el paquete x
también depende del paquete y
pero necesita una versión diferente de éste, podrás seguir ejecutando tu código sin problemas, porque x
buscará y
en su propia carpeta vendor
mientras que tu paquete buscará y
en la carpeta de proveedores de tu proyecto.
Ahora, un ejemplo práctico. Digamos que tienes esta estructura de carpetas:
$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
Si importamos github.com/user/package-one
desde dentro de main.go se resolvería a la versión de este paquete en el directorio vendor
en la misma carpeta:
$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
Ahora si importamos el mismo paquete en client.go
también se resolverá este paquete a la carpeta vendor
en su propio directorio:
$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 mismo ocurre al importar este paquete en el archivo 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
Entendiendo la gestión de dependencias
Ahora que hemos aprendido todas estas cosas sobre cómo Go
maneja las importaciones y las ventas ya es hora de que hablemos finalmente de la gestión de dependencias.
La herramienta que estoy utilizando actualmente para gestionar las dependencias en mis propios proyectos se llama godep
. Parece ser muy popular y funciona bien para mí, por lo tanto, te recomiendo que lo uses también.
Está construido alrededor de la forma en que funciona vendoring
. Todo lo que tienes que hacer para empezar a usarlo es usar el comando godep save
siempre que quieras guardar tus dependencias en tu carpeta vendor
.
Cuando ejecutes godep save
, godep
guardará una lista de tus dependencias actuales en Godeps/Godeps.json
y luego copiará su código fuente en la carpeta vendor
. También es importante notar que necesitas tener estas dependencias instaladas en tu máquina para que godep
pueda copiarlas.
Ahora puedes confirmar la carpeta vendor
y su contenido para asegurarte de que todo el mundo tendrá las mismas versiones de los mismos paquetes cuando ejecute tu paquete.
Otro comando interesante es godep restore
, que instalará las versiones especificadas en tu archivo Godeps/Godeps.json
en tu $GOPATH
.
Para actualizar una dependencia todo lo que tienes que hacer es actualizarla usando go get -u
(como hemos hablado antes) y luego ejecutar godep save
para que godep
actualice el archivo Godeps/Godeps.json
y copie los archivos necesarios en el directorio vendor
.
Algunas reflexiones sobre la forma en que Go maneja las dependencias
Al final de esta entrada del blog, también me gustaría añadir mi propia opinión sobre la forma en que Go maneja las dependencias.
Creo que la elección de Go de usar repositorios externos para manejar los espacios de nombres de los paquetes fue genial porque hace que toda la resolución de paquetes sea mucho más simple al unir los conceptos del sistema de archivos con los conceptos de espacios de nombres. Esto también hace que todo el ecosistema funcione de forma independiente porque ahora tenemos una forma descentralizada de obtener paquetes.
Sin embargo, la gestión descentralizada de paquetes tiene un coste, que es no poder controlar todos los nodos que forman parte de esta «red». Si alguien decide que va a sacar su paquete de github
, por ejemplo, entonces cientos, miles o incluso millones de construcciones pueden empezar a fallar de repente. Los cambios de nombre también podrían tener el mismo efecto.
Considerando los objetivos principales de Go esto tiene total sentido y es una compensación totalmente justa. Go se basa en la convención en lugar de la configuración y no hay manera más simple de manejar las dependencias que la forma en que lo hace actualmente.
Por supuesto, se podrían hacer algunas mejoras, como el uso de etiquetas git para obtener versiones específicas de los paquetes y permitir a los usuarios especificar qué versiones utilizarán sus paquetes. También sería genial poder obtener estas dependencias en lugar de comprobarlas en el control de fuentes. Esto nos permitiría evitar los diffs sucios que sólo contienen cambios en el código en el directorio vendor
y hacer que todo el repositorio sea más limpio y ligero.
¡Contáctate!
Si tienes alguna duda, pensamiento o si no estás de acuerdo con algo de lo que he escrito, por favor compártelo conmigo en los comentarios de abajo o contáctame en @thewizardlucas en twitter. Me encantaría escuchar lo que tienes que decir y hacer cualquier corrección si he cometido algún error.
Por último, pero no menos importante, asegúrate de echar un vistazo a esta impresionante charla de Wisdom Omuya en la GopherCon 2016 en la que explica cómo funciona Go Vendoring y también señala algunos detalles sobre su funcionamiento interno.
¡Gracias por leer esto!