Go modules: gestión de dependencias y versionado
1. Resumen
Desde Go 1.11, Go modules es el mecanismo oficial para gestionar dependencias. Desde Go 1.16, es el modo predeterminado, reemplazando completamente la forma antigua basada en GOPATH. Este artículo cubre todo el ciclo de vida de un módulo: inicialización, agregar dependencias, seleccionar y actualizar versiones, gestionar major version v2+, y trabajar con código local o repositorios privados. Después de leerlo, entenderás qué contienen go.mod y go.sum. También sabrás por qué Go elige una versión específica y cómo arreglar los errores de dependencias más comunes.
2. Inicializar módulos
Un módulo es un árbol de directorios con un archivo go.mod en la raíz. Crea un nuevo módulo usando go mod init:
mkdir order-service && cd order-service
go mod init github.com/nonla/order-service
Se crea el archivo go.mod:
module github.com/nonla/order-service
go 1.24
El significado de cada línea:
modulees el module path. Este es el prefijo de importación para todos los packages dentro del módulo. Si otros van a hacergo getde este módulo, esta ruta debe coincidir con la URL del repositorio.go 1.24es la versión mínima del lenguaje que requiere el módulo. Desde Go 1.21, esta línea es obligatoria. Si tu toolchain es más antiguo, Go descargará automáticamente el toolchain adecuado (cuandoGOTOOLCHAIN=auto) o reportará un error.
Nota: Para aplicaciones internas que nunca serán importadas desde afuera, puedes usar una ruta corta como
order-service. Sin embargo, usar una ruta con formato URL desde el principio evitará tener que cambiar masivamente los imports después.
3. Agregar dependencias
3.1. Usar go get
Supongamos que necesitas generar IDs para pedidos usando UUID. Agrega la librería github.com/google/uuid:
go get github.com/google/uuid@v1.6.0
Luego escribe código que use la librería:
// order/order.go
package order
import "github.com/google/uuid"
type Order struct {
ID string
Customer string
Total int64 // unidad: dong
}
func NewOrder(customer string, total int64) Order {
return Order{
ID: uuid.NewString(),
Customer: customer,
Total: total,
}
}
Ahora go.mod tiene un bloque require adicional:
module github.com/nonla/order-service
go 1.24
require github.com/google/uuid v1.6.0
También puedes omitir el paso de go get: escribe el import primero, luego ejecuta go mod tidy. Go encontrará automáticamente la última versión y la escribirá en go.mod.
3.2. El archivo go.sum
Al mismo tiempo, Go crea el archivo go.sum:
github.com/google/uuid v1.6.0 h1:<hash del contenido del módulo>
github.com/google/uuid v1.6.0/go.mod h1:<hash del archivo go.mod>
Cada dependencia tiene dos líneas. La primera línea es el hash de todo el contenido del módulo, la segunda es el hash del archivo go.mod en sí. Cuando descarga un módulo, Go compara el hash real con go.sum. También verifica con la base de datos de checksums pública (sum.golang.org). Si alguien modifica el contenido de un tag ya lanzado, la compilación fallará en lugar de usar silenciosamente código extraño.
Nota: Siempre haz commit de
go.modygo.sum. No agreguesgo.suma.gitignore. Si falta este archivo, el CI no podrá verificar las dependencias.
3.3. Dependencias directas e indirectas
Agrega testify para escribir pruebas:
go get github.com/stretchr/testify@v1.10.0
Ahora go.mod tendrá dos bloques require:
require (
github.com/google/uuid v1.6.0
github.com/stretchr/testify v1.10.0
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
El comentario // indirect marca módulos que tu código no importa directamente pero que necesita otra dependencia. Go los escribe en go.mod para que la compilación sea completamente reproducible. Gracias a esto, Go no necesita leer nuevamente el go.mod de cada dependencia para construir el gráfico.
4. Versiones y cómo Go elige versiones
4.1. Versionado semántico
Go modules usa versionado semántico con el formato vMAJOR.MINOR.PATCH:
PATCH(v1.6.0 → v1.6.1): corrige errores, no cambia la API.MINOR(v1.6.0 → v1.7.0): agrega características, sigue siendo compatible hacia atrás.MAJOR(v1 → v2): hay cambios que rompen la compatibilidad.
La versión debe ser una etiqueta de git con prefijo v. Si apuntas a un commit sin etiqueta, Go crea una pseudo-versión:
go get github.com/google/uuid@0f3b7a2
github.com/google/uuid v1.6.1-0.20240815101530-0f3b7a2c1d4e
La pseudo-versión consta de tres partes: la versión base, la marca de tiempo UTC del commit y los primeros 12 caracteres del hash. Gracias a esto, la pseudo-versión se ordena correctamente por tiempo.
4.2. Minimal Version Selection
Este es el mayor punto de diferencia con npm o Maven. Go utiliza el algoritmo Minimal Version Selection (MVS). Para cada módulo, Go elige la versión más alta entre las versiones requeridas, no la versión más reciente que existe.
Por ejemplo, tu proyecto tiene dos dependencias:
order-service
├── payment-sdk → require github.com/google/uuid v1.3.0
└── shipping-sdk → require github.com/google/uuid v1.5.0
Incluso si uuid v1.6.0 ya se ha lanzado, Go elige v1.5.0. La razón: es la versión más baja que satisface todos los requisitos. Las consecuencias:
- La compilación es reproducible. El lanzamiento de una nueva versión de una librería no hace que tu compilación cambie automáticamente.
- No necesitas un archivo de bloqueo separado.
go.modes suficiente para determinar las versiones. - Para usar una versión más reciente, debes actualizar deliberadamente.
Ver la versión real seleccionada con el comando:
go list -m all
github.com/nonla/order-service
github.com/davecgh/go-spew v1.1.1
github.com/google/uuid v1.5.0
...
4.3. Actualizar y degradar
Verifica qué dependencias tienen nuevas versiones:
go list -m -u all
github.com/google/uuid v1.5.0 [v1.6.0]
github.com/stretchr/testify v1.10.0
La versión entre corchetes es la versión más reciente a la que puedes actualizar. Comandos de actualización comúnmente usados:
go get github.com/google/uuid@latest # versión más reciente
go get github.com/google/uuid@v1.5.0 # a una versión específica (puede ser una degradación)
go get -u=patch ./... # solo actualiza PATCH para todas las dependencias
go get -u ./... # actualiza MINOR + PATCH para todas las dependencias
go get github.com/google/uuid@none # elimina completamente la dependencia
Nota:
go get -u ./...actualiza también las dependencias indirectas. El diff puede ser muy grande. En proyectos de producción, debes actualizar cada módulo, ejecutar pruebas, y luego hacer commit.
5. Major version v2 en adelante
Go aplica la regla de compatibilidad de imports: dos versiones con la misma ruta de importación deben ser compatibles hacia atrás. Por lo tanto, desde v2 en adelante, la major version es parte de la ruta del módulo.
Por ejemplo, con el router chi:
go get github.com/go-chi/chi/v5@v5.1.0
package main
import (
"net/http"
"github.com/go-chi/chi/v5"
)
func main() {
r := chi.NewRouter()
r.Get("/orders", func(w http.ResponseWriter, _ *http.Request) {
w.Write([]byte("lista de pedidos"))
})
http.ListenAndServe(":8080", r)
}
El sufijo /v5 está en la ruta de importación. El nombre del package sigue siendo chi, así que llamas a chi.NewRouter() como de costumbre. Como chi/v4 y chi/v5 tienen rutas diferentes, ambas versiones pueden coexistir en un binario. Esto te ayuda a migrar gradualmente en partes.
Si eres el autor de la librería y quieres lanzar v2:
module github.com/nonla/pricing/v2
go 1.24
Cambias la línea module a una ruta con sufijo /v2, actualizas los imports internos, y creas la etiqueta v2.0.0.
Nota: El error común es solo crear la etiqueta
v2.0.0pero olvidar cambiar la ruta del módulo. Entoncesgo get github.com/nonla/pricing@v2.0.0reportará un error comoinvalid version: module contains a go.mod file, so module path must match major version.
6. Comandos útiles de go mod
go mod tidy # agrega módulos faltantes, elimina módulos sin usar
go mod why -m gopkg.in/yaml.v3 # por qué este módulo está presente
go mod graph # imprime el gráfico completo de dependencias
go mod verify # verifica que la caché local coincida con go.sum
go mod download # descarga previamente dependencias (útil para capas Docker)
Ejemplo de salida de go mod why:
# gopkg.in/yaml.v3
github.com/nonla/order-service/order
github.com/stretchr/testify/assert
gopkg.in/yaml.v3
Leyendo de arriba hacia abajo: el package order (en las pruebas) importa testify/assert, y testify/assert a su vez importa yaml.v3.
Ejecuta go mod tidy antes de cada commit. En CI, puedes verificar si go.mod está limpio:
go mod tidy -diff
Desde Go 1.23, la bandera -diff imprime la diferencia y devuelve un código de salida diferente a 0 si el archivo no está limpio. Este comando no sobrescribe el archivo.
7. Trabajar con código local: replace y go.work
7.1. Directiva replace
Supongamos que estás editando la librería pricing en un directorio adyacente y quieres que order-service use la versión que estás editando:
require github.com/nonla/pricing v1.2.0
replace github.com/nonla/pricing => ../pricing
Go leerá el código de ../pricing en lugar de descargarlo de la red. replace también puede apuntar a un fork:
replace github.com/nonla/pricing => github.com/minh/pricing v1.2.1-fix
Nota:
replacesolo tiene efecto en el módulo principal. Si tu módulo es una librería, los usuarios no recibirán tureplace. Además, no hagas commit de unreplaceque apunte a una ruta local. La compilación en CI fallará porque el directorio../pricingno existe allí.
7.2. Espacios de trabajo con go.work
Desde Go 1.18, los espacios de trabajo (workspaces) son una forma más limpia de desarrollar múltiples módulos simultáneamente:
go work init ./order-service ./pricing
Se crea el archivo go.work:
go 1.24
use (
./order-service
./pricing
)
Cuando ejecutas go build o go test dentro del workspace, cualquier importación a github.com/nonla/pricing usará el código local. No necesitas modificar go.mod. Normalmente go.work no se hace commit; cada desarrollador lo crea según su propia organización de directorios.
7.3. exclude y retract
exclude se usa en el módulo principal para descartar una versión específica:
exclude github.com/google/uuid v1.4.0
retract es declarado por el autor de la librería para indicar que una versión ya lanzada no debe usarse:
retract v1.2.0 // cálculo incorrecto de IVA para pedidos internacionales
Después de lanzar una nueva versión que contiene esta línea, go get @latest ignorará v1.2.0. El comando go list -m -u también advertirá a quienes estén usando esa versión.
8. Módulos privados y proxy
Por defecto, Go descarga módulos a través de GOPROXY=https://proxy.golang.org,direct y verifica checksums mediante sum.golang.org. Para repositorios internos, el proxy público no puede acceder, y tampoco quieres exponer los nombres de los módulos. Declara:
go env -w GOPRIVATE=github.com/nonla/*,gitlab.nonla.internal/*
Los módulos que coincidan con los patrones en GOPRIVATE se descargarán directamente a través de git e ignorarán la base de datos de checksums. Aún necesitas configurar la autenticación de git, por ejemplo usando ~/.netrc o SSH:
git config --global url."git@github.com:".insteadOf "https://github.com/"
Si tu empresa tiene su propio proxy (Athens, Artifactory…), coloca ese proxy al principio de GOPROXY:
go env -w GOPROXY=https://goproxy.nonla.internal,https://proxy.golang.org,direct
9. Pruebas de comportamiento con dependencias
Escribe una prueba para NewOrder, usando testify que agregamos en la sección 3.3:
// order/order_test.go
package order
import (
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewOrder_GeneratesValidUniqueID(t *testing.T) {
first := NewOrder("Nguyễn Văn An", 250_000)
second := NewOrder("Trần Thị Bình", 120_000)
_, err := uuid.Parse(first.ID)
require.NoError(t, err)
assert.NotEqual(t, first.ID, second.ID)
assert.Equal(t, "Nguyễn Văn An", first.Customer)
assert.EqualValues(t, 250_000, first.Total)
}
Ejecuta la prueba:
go test ./...
ok github.com/nonla/order-service/order 0.004s
La prueba verifica tres cosas: que el ID es un UUID válido, que dos pedidos tienen IDs diferentes, y que los otros campos se asignan correctamente. require.NoError detiene la prueba inmediatamente si el ID no se puede parsear. assert registra el error y continúa ejecutando. La primera vez que ejecutas en una máquina nueva, Go descargará las dependencias con las versiones exactas en go.mod y verificará los hashes con go.sum antes de compilar.
10. Errores comunes
missing go.sum entry for module providing package:go.sumcarece de la línea correspondiente, generalmente porque alguien editógo.modmanualmente. Solución: ejecutago mod tidy.checksum mismatch: el contenido del módulo no coincide con el hash. Intentago clean -modcache. Si aún falla con un módulo público, considera esto como una señal grave y no elimines la línea dego.sum.go: updates to go.mod needed: CI generalmente se ejecuta con-mod=readonly, mientras quego.modno está limpio. Ejecutago mod tidylocalmente y luego haz commit.- Importar una major version incorrecta: usar
github.com/go-chi/chien lugar de.../chi/v5. Recibirás una versión v1 antigua o una pseudo-versión extraña. Verifica primero la ruta de importación. - Abuso de
vendor/:go mod vendorsigue siendo útil cuando necesitas compilar completamente offline. De lo contrario, el proxy combinado congo.sumya garantiza reproducibilidad, así que no necesitas hacer commit de miles de archivos vendor.
11. Conclusión
Go modules resuelve el problema de las dependencias con pocas ideas simples. go.mod declara los requisitos, go.sum asegura la integridad, y Minimal Version Selection permite compilaciones reproducibles sin necesidad de un archivo de bloqueo. La major version está en la ruta de importación, por lo que los cambios que rompen la compatibilidad siempre son visibles en el código.
Para tu trabajo diario, solo necesitas recordar cuatro hábitos:
- Usa
go get module@versionpara agregar o actualizar dependencias deliberadamente. - Ejecuta
go mod tidyantes de hacer commit. - Usa
go.worken lugar dereplacecuando desarrollas múltiples módulos simultáneamente. - Configura
GOPRIVATEpara repositorios internos.