En este artículo vamos a centrarnos en la gestión de las dependencias en golang. Un tema de bastante controversia, por no tener una solución estándar.
Introducción
Hace unos 3 años que llevo programando en golang, y siempre una de las discusiones de más relevancia ha sido el setup del proyecto y las dependencias de este.
Existen múltiples soluciones de funcionalidad similar (algunas compatibles entre ellas y otras más complejas de gestionar), lo que causa un problema cuando queremos realizar una correcta gestión de las dependencias.
Tener correctamente identificadas y versionadas nuestras dependencias, hace que un proceso de release sea mucho más cómodo, siendo totalmente repetible y obteniendo por lo tanto el mismo binario.
En Java, desde la llegada de Apache Maven, se dio un paso bastante grande en cuanto a tener localizadas nuestras librerías y poder acceder a ellas a repositorios de binarios, simplificando muchísimo los setup de los proyectos. Quizás en los últimos tiempos se le ha dado mucha más funcionalidad, adicionalmente a la gestión de dependencias.
En Javascript, la gestión de paquetes con npm es bastante sencilla y muchos compañeros ponen como ejemplo de dependencias bien gestionadas.
Hace ya algunos años, apareció Gradle, que tiende a ser una solución independiente del lenguaje. Aunque al inicio comenzó con Java, ya tiene en un plugin para golang, Gogradle, y hay mucha más funcionalidad que se puede implementar codificando con Groovy.
También aparece Bazel, aunque no he tenido la posibilidad de probarlo y de momento no tiene soporte para golang, con lo que lo dejaré para otro artículo.
Después de este breve resumen, vamos al lio…
Los inicios…
Cuando empecé a escribir las primeras aplicaciones, la solución aparentemente más utilizada era godep, aunque su trabajo en el día a día era algo enrevesado, y a veces tenía problemas con algunas dependencias como os comentaré más adelante. Como dato adicional, kubernetes parece que sigue usándolo.
El siguiente candidato fue Glide, simplificaba algo más la gestión, por lo menos la hacía mucho más intuitiva pero tampoco se libraba de problemas, aunque he de decir que era mucho más estable.
En el momento de escribir este artículo, iba a centrarme en estas dos, pero desde el blog de Golang vi una noticia que me llamó la atención, en la versión 1.11 aparecerán los Golang Modules, del cual haré un artículo cuando llegue el momento, esto enlazó directamente con el gestor de dependencias experimental dep.
De ahí nace este artículo.
Los problemas…
Cuando trabajamos con golang, los gestores de dependencias que usan el directorio vendor para almacenar el código fuente de las dependencias, siempre sale el mismo debate: ¿subimos el directorio al repositorio de nuestro código o no?
Veamos las que ventajas/inconvenientes de cada solución. Aunque antes de nada un pequeño disclaimer:
Hay diversos foros en los que hay debate en cuanto al versionado, hay compañeros que opinan que las dependencias siempre deben estar en la última versión /commit /rama master, con lo que versionar con tags no es necesario. Adicionalmente siempre se puede usar el hash del commit.
En contraparte (y mi opinión), es que toda aplicación debe tener definidas las dependencias con un correcto versionado utilizando semver.
Personalmente y si está bien utilizado (por nosotros y por las librerías que usemos), mantener la versión mayor e ir incrementando el minor/patch (que nos asegura retro-compatibilidad) es la mejor opción.
Para ello, tendremos que tener una suite de test que aseguren que nada se rompe después de un incremento de versión de una dependencia.
Vendor en repositorio
Puesto que algunas librerías/frameworks no están correctamente versionados (si, esto pasa…) o carecen un correcto ciclo de vida por lo anteriormente mencionado.
Hay dependencias de las que únicamente podremos tener el hash del commit, y esto asegurando que el repositorio no desaparezca o que se haya migrado.
Por otro lado, como las dependencias son código fuente, tampoco deben ocupar mucho espacio (aunque hay casos en los que si).
Quizás en proyectos muy antiguos sea necesario mantenerlo de esta forma, hasta que nuestro software esté lo suficientemente testeado para poder migrar a otra solución.
Como ventaja es que nuestro CI/CD no tendrá que descargar las dependencias, siendo el proceso de test && build mucho más rápido.
Vendor fuera del repositorio
La principal justificación de esta solución, es que sólo nuestro código fuente debe estar en nuestro repositorio.
Las dependencias deben descargarse cada vez que se quiere trabajar (al menos la primera vez).
Como contrapunto de esta solución es que nuestro CI/CD descargará cada vez las dependencias, siendo un proceso muy parecido a como sería con otros lenguajes y gestores de dependencias.
También puede que existan dependencias que desaparezcan o que no mantengan retro-compatibilidad. Mi recomendación, es que si el código es libre, se realice un fork del repositorio y se versione (y si se quiere colaborar en mejorarlo, realizar contribuciones).
Dep, ¿la solución?
En absoluto, aunque si se están dando pasos para que sea un estándar.
En las primeras pruebas que he realizado con el código del anterior artículo, ha sido muy limpia, aunque he de decir que el código tampoco tiene muchas dependencias.
Setup
Lo primero, comentaros que hay varias formas de instalar dep: https://golang.github.io/dep/docs/installation.html
Es necesario tener definido tanto las variables $GOROOT
como el $GOPATH
.
En mi caso, como estoy desde una distribución linux optaré por usar el siguiente comando:
$ curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
Teniendo en cuenta mi $GOPATH
, he realizado un pequeño script para simplificarme la vida, cada vez que quiero trabajar con golang:
#!/bin/bash
export GOPATH=~/gopath/
export PATH=$PATH:$GOPATH/bin
NOTA: A veces trabajo con diferentes workspace / gopaths, así que prefiero mantenerlo separado.
Haciendo un source del binario (yo lo he llamado gopathinit.sh
) en nuestro directorio de proyecto, y ejecutando el binario de dep (localizado en $GOPATH
/bin):
$ chmod +x gopathinit.sh
$ source gopathinit.sh
$ dep
Dep is a tool for managing dependencies for Go projects
Usage: "dep [command]"
Commands:
init Set up a new Go project, or migrate an existing one
status Report the status of the project's dependencies
ensure Ensure a dependency is safely vendored in the project
version Show the dep version information
check Check if imports, Gopkg.toml, and Gopkg.lock are in sync
Examples:
dep init set up a new project
dep ensure install the project's dependencies
dep ensure -update update the locked versions of all dependencies
dep ensure -add github.com/pkg/errors add a dependency to the project
Use "dep help [command]" for more information about a command.
Vemos que el comando ha quedado correctamente instalado.
Configuración del proyecto
Antes de nada, si seguisteis el anterior artículo, deberiais tener la siguiente estructura de directorios desde el raiz de nuestro proyecto:
$ tree
.
├── gopathinit.sh
├── main.go
└── users
└── users.go
1 directory, 3 files
Como veis, no hay nada adicional a lo que ya hemos creado.
En el siguiente paso si ejecutamos dep init
en el raiz de nuestro proyecto, dep, analizará las dependencias de nuestro código:
$ dep init
Using ^1.6.2 as constraint for direct dep github.com/gorilla/mux
Locking in v1.6.2 (e3702be) for direct dep github.com/gorilla/mux
Locking in v1.1.1 (08b5f42) for transitive dep github.com/gorilla/context
Si volvemos a revisar nuestro directorio de proyecto:
$ tree
.
├── gopathinit.sh
├── Gopkg.lock
├── Gopkg.toml
├── main.go
├── users
│ └── users.go
└── vendor
└── github.com
└── gorilla
├── context
│ ├── context.go
│ ├── doc.go
│ ├── LICENSE
│ └── README.md
└── mux
├── context_gorilla.go
├── context_native.go
├── doc.go
├── ISSUE_TEMPLATE.md
├── LICENSE
├── middleware.go
├── mux.go
├── README.md
├── regexp.go
├── route.go
└── test_helpers.go
6 directories, 20 files
Vemos dos archivos, Gopkg.lock
y Gopkg.toml
, además del mencionado directorio vendor
. Vamos a examinarlos:
Gopkg.toml
# Gopkg.toml example
#
# Refer to https://golang.github.io/dep/docs/Gopkg.toml.html
# for detailed Gopkg.toml documentation.
#
# required = ["github.com/user/thing/cmd/thing"]
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
#
# [[constraint]]
# name = "github.com/user/project"
# version = "1.0.0"
#
# [[constraint]]
# name = "github.com/user/project2"
# branch = "dev"
# source = "github.com/myfork/project2"
#
# [[override]]
# name = "github.com/x/y"
# version = "2.4.0"
#
# [prune]
# non-go = false
# go-tests = true
# unused-packages = true
[[constraint]]
name = "github.com/gorilla/mux"
version = "1.6.2"
[prune]
go-tests = true
unused-packages = true
Como vemos, directamente nos ha colocado la dependencia que ya teníamos con gorilla/mux
, es aquí donde nosotros podremos cambiar la versión de las dependencias siempre y cuando queramos forzar una versión.
Gopkg.lock
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
digest = "1:c79fb010be38a59d657c48c6ba1d003a8aa651fa56b579d959d74573b7dff8e1"
name = "github.com/gorilla/context"
packages = ["."]
pruneopts = "UT"
revision = "08b5f424b9271eedf6f9f0ce86cb9396ed337a42"
version = "v1.1.1"
[[projects]]
digest = "1:e73f5b0152105f18bc131fba127d9949305c8693f8a762588a82a48f61756f5f"
name = "github.com/gorilla/mux"
packages = ["."]
pruneopts = "UT"
revision = "e3702bed27f0d39777b0b37b664b6280e8ef8fbf"
version = "v1.6.2"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
input-imports = ["github.com/gorilla/mux"]
solver-name = "gps-cdcl"
solver-version = 1
No se debe modificar, ya que contiene adicionalmente a la versión, los hash de los commit de las dependencias y sus transitivas, en este caso github.com/gorilla/context
.
Con esto ya tenemos el setup de un proyecto existente.
Añadir dependencias
Añadir dependencias es sencillo, por ejemplo, si queremos añadir la libreria logrus, primero debemos instalarla en nuestro $GOPATH
:
$ go get github.com/sirupsen/logrus
Y modificar nuestro main.go
:
package main
import (
"encoding/json"
log "github.com/sirupsen/logrus"
"net/http"
"github.com/gorilla/mux"
. "rest-api/users"
"strconv"
)
...
Como veis, hemos cambiado el import para usar logrus:
Ahora indicamos la versión en Gopkg.toml
:
# Gopkg.toml example
#
# Refer to https://golang.github.io/dep/docs/Gopkg.toml.html
# for detailed Gopkg.toml documentation.
#
# required = ["github.com/user/thing/cmd/thing"]
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
#
# [[constraint]]
# name = "github.com/user/project"
# version = "1.0.0"
#
# [[constraint]]
# name = "github.com/user/project2"
# branch = "dev"
# source = "github.com/myfork/project2"
#
# [[override]]
# name = "github.com/x/y"
# version = "2.4.0"
#
# [prune]
# non-go = false
# go-tests = true
# unused-packages = true
[[constraint]]
name = "github.com/gorilla/mux"
version = "1.6.2"
[[constraint]]
name = "github.com/sirupsen/logrus"
version = "v1.3.0"
[prune]
go-tests = true
unused-packages = true
Por último ejecutamos para que la dependencia se registre:
$ dep ensure
Con esto la librería está en nuestro vendor
y aparece en nuestro Gopkg.lock
:
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
digest = "1:c79fb010be38a59d657c48c6ba1d003a8aa651fa56b579d959d74573b7dff8e1"
name = "github.com/gorilla/context"
packages = ["."]
pruneopts = "UT"
revision = "08b5f424b9271eedf6f9f0ce86cb9396ed337a42"
version = "v1.1.1"
[[projects]]
digest = "1:e73f5b0152105f18bc131fba127d9949305c8693f8a762588a82a48f61756f5f"
name = "github.com/gorilla/mux"
packages = ["."]
pruneopts = "UT"
revision = "e3702bed27f0d39777b0b37b664b6280e8ef8fbf"
version = "v1.6.2"
[[projects]]
digest = "1:0a69a1c0db3591fcefb47f115b224592c8dfa4368b7ba9fae509d5e16cdc95c8"
name = "github.com/konsorten/go-windows-terminal-sequences"
packages = ["."]
pruneopts = "UT"
revision = "5c8c8bd35d3832f5d134ae1e1e375b69a4d25242"
version = "v1.0.1"
[[projects]]
digest = "1:87c2e02fb01c27060ccc5ba7c5a407cc91147726f8f40b70cceeedbc52b1f3a8"
name = "github.com/sirupsen/logrus"
packages = ["."]
pruneopts = "UT"
revision = "e1e72e9de974bd926e5c56f83753fba2df402ce5"
version = "v1.3.0"
[[projects]]
branch = "master"
digest = "1:38f553aff0273ad6f367cb0a0f8b6eecbaef8dc6cb8b50e57b6a81c1d5b1e332"
name = "golang.org/x/crypto"
packages = ["ssh/terminal"]
pruneopts = "UT"
revision = "ff983b9c42bc9fbf91556e191cc8efb585c16908"
[[projects]]
branch = "master"
digest = "1:5ee4df7ab18e945607ac822de8d10b180baea263b5e8676a1041727543b9c1e4"
name = "golang.org/x/sys"
packages = [
"unix",
"windows",
]
pruneopts = "UT"
revision = "48ac38b7c8cbedd50b1613c0fccacfc7d88dfcdf"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
input-imports = [
"github.com/gorilla/mux",
"github.com/sirupsen/logrus",
]
solver-name = "gps-cdcl"
solver-version = 1
Por último, si queremos probar si nuestro setup está correcto, podemos borrar el directorio vendor
y las dependencias en $GOPATH
. Adicionalmente hay un directorio: $GOPATH/pkg/dep
que podriamos borrar tambien.
Ejecutamos de nuevo dep ensure
y las dependencias vuelven a aparecer.
Conclusiones
Esperemos que con dep y los Golang Modules, la gestión de dependencias trate de ser un estandar que llegue a la comunidad y sea bien recibido.
Aprovecho para felicitaros el año :)
¡¡Nos leemos en el siguiente artículo!!