Módulos en Go: qué son, cómo funcionan y por qué ya no deberías aprender GOPATH
Guía práctica de go modules: go.mod, go.sum, dependencias, versionado y errores habituales. Olvida GOPATH.

Muchos tutoriales de Go siguen explicando GOPATH como si fuera 2017. No lo hagas. Desde Go 1.16, los módulos son el sistema por defecto para gestionar dependencias, y desde Go 1.22 GOPATH como workspace de desarrollo es básicamente un artefacto histórico. Si estás aprendiendo Go ahora, aprender con GOPATH como centro es aprender con una foto vieja. Los módulos resuelven el problema de dependencias de forma explícita, reproducible y sin magia de directorios.
Si vienes de otros ecosistemas, piensa en go.mod como tu package.json, Cargo.toml o build.gradle.kts, pero más simple y sin un gestor de paquetes separado. Todo está integrado en el propio comando go.
Si todavía estás decidiendo si Go merece tu tiempo, echa un vistazo a cómo empezar con Go antes de seguir aquí.
Qué es un módulo en Go
Un módulo es una colección de paquetes Go que se versionan y distribuyen juntos. En la práctica, un módulo es un directorio que contiene un archivo go.mod en su raíz. Ese archivo define tres cosas fundamentales:
- El module path: la identidad del módulo, que también es la ruta base para importar sus paquetes.
- La versión de Go mínima que necesita.
- Las dependencias: qué otros módulos necesita y en qué versión exacta.
Cada repositorio suele contener un solo módulo. Puedes tener varios, pero no lo hagas salvo que tengas una razón muy clara (monorepos con componentes independientes, por ejemplo).
La diferencia clave con GOPATH: antes, todo tu código y todas tus dependencias vivían en un único árbol de directorios bajo $GOPATH/src. No había versionado real, no había reproducibilidad, y dos proyectos que necesitaran versiones diferentes de la misma librería… pues mala suerte. Los módulos eliminan todo eso.
Anatomía de go.mod
Un go.mod típico tiene este aspecto:
module github.com/tu-usuario/mi-proyecto
go 1.22
require (
github.com/gin-gonic/gin v1.10.0
github.com/jackc/pgx/v5 v5.6.0
go.uber.org/zap v1.27.0
)
require (
// indirect dependencies
github.com/bytedance/sonic v1.11.8 // indirect
github.com/gabriel-vasile/mimetype v1.4.4 // indirect
// ... más dependencias indirectas
)Desglose:
module github.com/tu-usuario/mi-proyecto: el path del módulo. Si tu código va a ser importado por otros, esto debe coincidir con la URL del repositorio. Si es un binario que nadie va a importar, puedes poner lo que quieras, pero la convención sigue siendo usar la URL del repo.go 1.22: la versión mínima de Go. No es decorativa. Go usa esta directiva para decidir qué funcionalidades del lenguaje están disponibles y cómo se resuelven las dependencias.require: las dependencias directas e indirectas. Go separa las directas de las indirectas con un comentario// indirect.
Las dependencias indirectas son módulos que tus dependencias directas necesitan, pero que tú no importas directamente. Go las registra en go.mod para garantizar builds reproducibles.
Crear tu primer módulo: go mod init
Crear un módulo es un solo comando:
mkdir mi-proyecto
cd mi-proyecto
go mod init github.com/tu-usuario/mi-proyectoEso genera un go.mod mínimo:
module github.com/tu-usuario/mi-proyecto
go 1.22A partir de aquí ya puedes crear archivos .go, importar paquetes estándar y compilar. Cuando añadas un import de un paquete externo, Go descargará la dependencia automáticamente al compilar o al ejecutar go mod tidy.
Un detalle que confunde a gente que viene de Node o Python: no necesitas un comando tipo npm install o pip install antes de escribir código. Escribes el import, ejecutas go build o go run, y Go resuelve la dependencia. Así de directo.
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "pong"})
})
fmt.Println("Servidor arrancando en :8080")
r.Run()
}Al ejecutar go run . por primera vez, Go descarga Gin y todas sus dependencias transitivas, actualiza go.mod y genera go.sum.
Si necesitas una visión más amplia de cómo organizar el código dentro del módulo, mira estructura de proyecto.
Añadir dependencias: go get y go mod tidy
Hay dos formas principales de añadir dependencias.
go get
go get descarga un módulo y lo añade a tu go.mod:
go get github.com/jackc/pgx/v5@latestPuedes especificar versiones concretas:
go get github.com/jackc/pgx/v5@v5.6.0
go get github.com/jackc/pgx/v5@v5.5.0 // downgrade
go get github.com/jackc/pgx/v5@abc1234 // commit específicoDesde Go 1.18, go get ya no compila ni instala binarios. Solo modifica go.mod y go.sum. Si quieres instalar una herramienta CLI, usa go install:
go install golang.org/x/tools/gopls@latestEsta distinción es importante. go get gestiona dependencias de tu proyecto. go install instala ejecutables.
go mod tidy
go mod tidy es el comando que más vas a usar en el día a día. Hace dos cosas:
- Añade las dependencias que tu código importa pero que no están en
go.mod. - Elimina las dependencias que están en
go.modpero que tu código ya no usa.
go mod tidyMi recomendación: ejecútalo siempre antes de hacer commit. Es la forma más fiable de mantener go.mod limpio y sincronizado con tu código real. No te fíes de que go build ya lo haya hecho todo; go build añade dependencias que faltan, pero no elimina las que sobran.
go.sum: qué es y por qué no deberías añadirlo a .gitignore
Cuando Go descarga un módulo, calcula un hash criptográfico de su contenido y lo guarda en go.sum. El archivo tiene este aspecto:
github.com/gin-gonic/gin v1.10.0 h1:ABC123...=
github.com/gin-gonic/gin v1.10.0/go.mod h1:DEF456...=Cada entrada tiene dos hashes: uno para el contenido del módulo completo y otro para su go.mod. Esto permite verificar la integridad de las dependencias sin descargarlas enteras.
La pregunta que todo el mundo hace: “Si ya tengo las versiones en go.mod, por qué necesito go.sum?”
Porque go.mod dice qué versión usar. go.sum verifica que esa versión no ha sido manipulada. Es una medida de seguridad. Si alguien compromete un repositorio y publica una versión v1.10.0 con contenido diferente, el hash no coincidirá y Go se negará a compilar.
go.sum va al repositorio. Siempre. No lo metas en .gitignore. La seguridad de tu cadena de dependencias depende de él. Go además consulta el Go checksum database para validar hashes contra un registro público, pero go.sum local es tu primera línea de defensa.
Un error habitual: hacer merge de ramas y tener conflictos en go.sum. La solución es sencilla: acepta cualquiera de las dos versiones y ejecuta go mod tidy. Go regenerará los hashes correctos.
Versionado semántico en Go: la convención del import path
Go adopta versionado semántico (semver), pero le añade una regla propia que es única en el ecosistema: a partir de v2, la versión major forma parte del import path.
Esto significa que:
import "github.com/jackc/pgx" // v0.x o v1.x
import "github.com/jackc/pgx/v5" // v5.xSon, a efectos del compilador, módulos diferentes. Puedes importar ambos en el mismo proyecto sin conflicto. Esto es intencionado: Go trata un cambio de major version como un módulo nuevo, lo que evita el infierno de dependencias incompatibles.
Para los mantenedores de librerías, esto implica que al publicar v2 de tu módulo, tienes que:
- Actualizar el
modulepath engo.modpara incluir/v2. - Actualizar todos los imports internos.
- Crear el tag
v2.0.0.
// go.mod para v2
module github.com/tu-usuario/mi-libreria/v2
go 1.22Es más trabajo que en npm o Cargo, donde simplemente cambias el número de versión. Pero el resultado es que en Go es literalmente imposible que una actualización de major version rompa tu build sin que tú la hayas importado explícitamente. No existe el equivalente a npm install rompiendo todo porque una dependencia transitiva subió de major.
Pre-release y pseudo-versiones
Si necesitas usar un commit que no tiene tag, Go genera una pseudo-versión:
require github.com/algo/algo v0.0.0-20240115140000-abc1234def56El formato es vX.Y.Z-YYYYMMDDHHMMSS-commitHash. No lo escribas a mano. Usa go get github.com/algo/algo@abc1234 y Go la genera por ti.
La directiva replace: reemplazar dependencias
replace es una de las herramientas más útiles de go.mod y una de las peor documentadas. Permite redirigir un módulo a otra ubicación o versión.
Desarrollo local
El caso de uso más común es trabajar con un fork local o una librería que estás modificando al mismo tiempo que tu proyecto:
module github.com/tu-usuario/mi-proyecto
go 1.22
require github.com/tu-usuario/mi-libreria v1.3.0
replace github.com/tu-usuario/mi-libreria => ../mi-libreriaCon esto, Go usa el código local en ../mi-libreria en lugar de descargar la versión publicada. Perfecto para desarrollo, pero nunca publiques un módulo con un replace apuntando a un path local. Solo funciona en tu máquina.
Forks y correcciones
Si necesitas usar un fork de una dependencia porque tiene un bug corregido que aún no se ha publicado:
replace github.com/libreria-original/algo => github.com/tu-fork/algo v1.3.1-fixRetract: marcar versiones como defectuosas
Desde Go 1.16, los mantenedores pueden marcar versiones como retracted en su go.mod:
retract (
v1.2.0 // contiene un bug crítico en el parser
[v1.3.0, v1.3.5] // rango de versiones defectuosas
)go get evitará automáticamente versiones retracted y te advertirá si ya las estás usando.
Módulos privados: GOPRIVATE y repos empresariales
Por defecto, Go intenta resolver módulos a través del proxy público (proxy.golang.org) y validar sus hashes contra la checksum database (sum.golang.org). Eso no funciona con repositorios privados.
La variable de entorno GOPRIVATE le dice a Go qué módulos debe resolver directamente contra el repositorio, saltándose el proxy y la checksum database:
go env -w GOPRIVATE="github.com/tu-empresa/*,gitlab.empresa.com/*"Para patterns más complejos, hay dos variables adicionales:
GONOSUMCHECK: módulos que no se validan contra la checksum database (pero sí usan el proxy).GONOPROXY: módulos que no pasan por el proxy (pero sí se validan contra la checksum database).
GOPRIVATE es equivalente a poner el mismo valor en ambas.
Autenticación con repos privados
Go usa Git bajo el capó para clonar repositorios. Si tu repo privado usa HTTPS, necesitas configurar Git para que se autentique:
// En ~/.gitconfig o ~/.config/git/config
[url "https://oauth2:TU_TOKEN@gitlab.empresa.com/"]
insteadOf = https://gitlab.empresa.com/Para GitHub, otra opción es configurar gh auth o usar un .netrc:
// ~/.netrc
machine github.com
login tu-usuario
password ghp_TU_TOKEN_PERSONALSi trabajas con SSH:
[url "ssh://git@github.com/"]
insteadOf = https://github.com/Un problema recurrente en CI/CD: el pipeline no tiene acceso al repositorio privado. La solución habitual es inyectar un token como variable de entorno y configurar el insteadOf en el step de setup.
Errores habituales y cómo solucionarlos
Estos son los errores con módulos que más he visto, tanto en código propio como ajeno.
”cannot find module providing package X”
main.go:5:2: no required module provides package github.com/algo/algo; to add it:
go get github.com/algo/algoEl import existe en tu código pero la dependencia no está en go.mod. Solución:
go get github.com/algo/algo
// o mejor:
go mod tidy“ambiguous import”
Ocurre cuando dos módulos exportan el mismo package path. Suele pasar con módulos que han migrado a v2 pero tienes imports mezclados. Revisa tus imports y asegúrate de que todos apuntan a la misma major version.
”checksum mismatch”
verifying github.com/algo/algo@v1.2.0: checksum mismatchEl hash del módulo descargado no coincide con el registrado en go.sum. Causas posibles:
- Caché corrupta:
go clean -modcachey vuelve a descargar. - El módulo ha sido alterado: alguien republicó una versión con contenido diferente. Esto no debería pasar con módulos públicos gracias a la checksum database, pero puede ocurrir con módulos privados.
- Conflicto de merge en go.sum: resuelve el conflicto y ejecuta
go mod tidy.
”module declares its path as X but was required as Y”
El go.mod del módulo que estás importando dice un module path diferente al que usaste en tu require o import. Esto pasa con forks: clonas un repo, cambias el código, pero no actualizas el module line de su go.mod. El módulo sigue diciendo que es github.com/original/repo, pero tú lo importas como github.com/tu-fork/repo. Usa replace para solventarlo.
”go.mod has post-v0 module path but no major version suffix”
Tu go.mod dice module github.com/algo/algo pero el tag es v2.0.0 o superior. A partir de v2, el module path debe incluir /v2. Es una regla que Go aplica estrictamente.
go.sum out of date
go: updates to go.sum needed, disabled by -mod=readonlyAparece en CI cuando alguien hizo cambios en dependencias pero olvidó ejecutar go mod tidy antes del commit. Solución: ejecuta go mod tidy localmente y haz commit de los cambios en go.mod y go.sum.
go mod tidy, go mod vendor, go mod download
Tres subcomandos que suenan parecidos pero hacen cosas diferentes.
go mod tidy
Ya lo hemos visto: sincroniza go.mod y go.sum con los imports reales de tu código. Añade lo que falta, elimina lo que sobra. Es el comando que más usarás.
go mod tidyTiene una opción útil: go mod tidy -v te muestra qué módulos ha añadido o eliminado. Úsalo cuando go mod tidy haga cambios inesperados y quieras entender por qué.
go mod vendor
Copia todas las dependencias a un directorio vendor/ dentro de tu proyecto:
go mod vendorCon vendor/, tu proyecto se puede compilar sin acceso a internet y sin depender de proxy.golang.org. Es útil en:
- Entornos con acceso restringido a internet (air-gapped).
- Cuando quieres tener control total sobre el código de terceros.
- Algunos pipelines de CI donde no quieres descargar dependencias en cada build.
Para compilar usando el vendor directory:
go build -mod=vendor ./...Desde Go 1.22, si existe un directorio vendor/, Go lo usa automáticamente. No necesitas el flag -mod=vendor explícitamente.
Mi opinión: vendor tiene sentido en proyectos que despliegan en entornos restrictivos o que necesitan auditorías de seguridad sobre el código de terceros. Para el resto, el módulo cache y el proxy son suficientes. Un directorio vendor/ con miles de archivos ensucia el repositorio y hace que los diffs sean ilegibles cuando actualizas dependencias.
go mod download
Descarga todas las dependencias a la caché local sin compilar nada:
go mod downloadEs especialmente útil en Dockerfiles, donde puedes aprovechar la caché de capas:
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o /app ./cmd/serverAl copiar primero solo go.mod y go.sum, Docker reutiliza la capa de go mod download mientras no cambien las dependencias. Esto acelera drásticamente los builds cuando solo cambias código fuente.
go mod graph y go mod why
Dos comandos de diagnóstico que poca gente conoce pero que son muy útiles:
go mod graphImprime el grafo completo de dependencias en formato texto. Es útil para entender por qué una dependencia transitiva está ahí.
go mod why github.com/algo/algoTe dice exactamente qué cadena de imports provoca que ese módulo sea una dependencia. Cuando ves un módulo en go.mod que no reconoces y quieres saber quién lo trajo, go mod why te da la respuesta.
El workspace mode: go.work
Desde Go 1.18, existe un modo adicional: workspaces. Un archivo go.work en un directorio padre te permite trabajar con múltiples módulos simultáneamente sin usar replace:
go 1.22
use (
./mi-proyecto
./mi-libreria
)Con esto, si mi-proyecto importa mi-libreria, Go usa automáticamente la copia local. Es más limpio que replace para desarrollo multimódulo, porque no modificas go.mod de ningún proyecto.
go work init ./mi-proyecto ./mi-libreriaNo subas go.work al repositorio (a menos que todo tu equipo trabaje con la misma estructura de directorios). go.work es una herramienta de desarrollo local, como un .env. Añádelo a .gitignore.
Para más detalles sobre el comando go y todas sus opciones, revisa el artículo dedicado.
Olvida GOPATH y no mires atrás
Si hay algo que me gustaría haberle dicho a mi yo de hace unos años es que dejara de pelear con GOPATH y $GOPATH/src/github.com/.... Los módulos cambiaron la forma de trabajar con dependencias en Go de raíz: go.mod define tu módulo de forma explícita, go.sum garantiza la integridad de todo lo que descargas, y go mod tidy se encarga de mantener ambos ficheros limpios. No necesitas más.
El resto del sistema encaja de forma natural una vez que interiorizas esas tres piezas. go get para añadir o actualizar dependencias, go install para binarios, replace para desarrollo local y forks, GOPRIVATE para repos privados, y vendor/ solo cuando tienes una razón concreta para usarlo. Las versiones v2+ con su import path diferente parecen raras al principio, pero cuando entiendes que es un mecanismo para evitar incompatibilidades silenciosas, deja de molestar.
No es el sistema de dependencias más flexible del mundo. Pero esa rigidez es precisamente lo que hace que funcione: builds reproducibles, sin sorpresas entre máquinas, sin configuración mágica de directorios. Después de haber trabajado con Maven, pip y npm, valoro mucho que go mod tidy haga lo correcto sin que tenga que pensar en ello.


