Cuando trabajas en un monorepo o necesitas desarrollar una librería y un microservicio simultáneamente, te encuentras con un problema de resolución de dependencias. Si intentas usar un módulo local (example.com/lib) que aún no ha sido publicado, la única forma tradicional era usar la directiva replace en el go.mod del consumidor. Sin embargo, esto es un error táctico: replace modifica permanentemente el archivo go.mod, lo que significa que si subes ese cambio, romperás el build de cualquier otro desarrollador o del CI/CD que no tenga exactamente la misma estructura de carpetas que tú.
go.work [disponible desde Go 1.18] resuelve esto introduciendo el concepto de Workspaces. Un Workspace es un conjunto de módulos que se tratan como una única unidad de compilación para el entorno local. En lugar de contaminar el go.mod con rutas relativas, el archivo go.work le indica al Go toolchain: “cuando busques dependencias para estos módulos, mira también en estas carpetas locales”. Esto permite que el grafo de dependencias se resuelva de forma transparente sin tocar los archivos de configuración de los módulos individuales.
Esto es fundamental en entornos de microservicios donde una librería interna cambia constantemente y necesitas probar sus efectos en tres servicios distintos sin hacer commits constantes de cambios en las versiones o usar replace. Si usas mal esta herramienta —por ejemplo, si cometes el error de incluir el go.work en tu repositorio de Git o si intentas usar replace para rutas locales en el go.mod— terminarás con un sistema de build frágil, difícil de reproducir en entornos de producción y propenso a errores de “package not found” en el servidor de integración continua.
// Estructura de archivos simulada en un monorepo:
// /monorepo
// ├── go.work
// ├── pkg/logger/
// │ ├── go.mod
// │ └── logger.go
// └── cmd/api/
// ├── go.mod
// └── main.go
// --- Archivo: /monorepo/pkg/logger/go.mod ---
// module example.com/logger
// --- Archivo: /monorepo/pkg/logger/logger.go ---
package logger
import "fmt"
func Log(msg string) {
fmt.Printf("[LOG]: %s\n", msg)
}
// --- Archivo: /monorepo/pkg/logger/go.mod ---
// module example.com/logger
// go 1.23
// --- Archivo: /monorepo/cmd/api/go.mod ---
// module example.com/api
// go 1.23
// require example.com/logger v0.0.0
// (Nota: No hay 'replace' aquí, el módulo es independiente)
// --- Archivo: /monorepo/cmd/api/main.go ---
package main
import (
"example.com/logger" // Importación limpia, sin rutas relativas
)
func main() {
logger.Log("Servidor API iniciado con Workspace")
}
// --- Archivo: /monorepo/go.work ---
// go 1.23
use (
./cmd/api
./pkg/logger
)
/*
Para ejecutar este ejemplo desde la raíz (/monorepo):
$ go run ./cmd/api/main.go
// Salida: [LOG]: Servidor API iniciado con Workspace
*/
Desglose del ejemplo
En la estructura presentada, tenemos dos módulos independientes: example.com/logger (una librería) y example.com/api (el ejecutable). Si intentas ejecutar go run ./cmd/api/main.go sin el archivo go.work, el compilador fallará porque no encontrará la dependencia example.com/logger en ningún proxy de Go ni en el cache local.
El corazón de la solución es el archivo go.work en la raíz. Al definir use ( ./cmd/api ./pkg/logger ), le estamos creando un grafo de módulos unificado al runtime de Go. Cuando el compilador analiza import "example.com/logger" en el paquete api, el toolchain consulta el workspace, identifica que la ruta example.com/logger coincide con el módulo definido en ./pkg/logger y utiliza ese código fuente local directamente.
Fíjate que cmd/api/go.mod sigue siendo un archivo limpio. No contiene la directiva replace. Esto es vital: el módulo api es “puro” y su configuración de dependencias es idéntica a la que tendrá cuando sea desplegado en producción. El workspace es una capa de orquestación puramente local que solo existe en tu máquina de desarrollo para facilitar el ciclo de edit-test-debug.
Si estuvieras trabajando con decenas de módulos, podrías usar go work sync [disponible desde Go 1.21] para sincronizar las versiones de las dependencias y asegurar que el archivo go.work refleje correctamente lo que el entorno necesita.
El error frecuente
Un error clásico que corrompe el flujo de trabajo de todo el equipo es cometer el pecado de incluir el archivo go.work en el sistema de control de versiones (Git).
// ERROR: No comitees esto en tu repositorio.
// --- Archivo: /monorepo/go.work ---
use (
./pkg/logger
/Users/tu-usuario/proyectos/otra-cosa/lib // Esto romperá el build de tus compañeros
)
Si cometes este error, estás forzando a tus compañeros a tener exactamente la misma estructura de directorios que tú, o peor aún, les estarás inyectando dependencias de tus carpetas personales. El go.work debe estar siempre en el .gitignore.
Otro error es intentar arreglar la falta de un Workspace usando replace en el go.mod del servicio:
// ERROR: Esto es "veneno" para el CI/CD // --- Archivo: /monorepo/cmd/api/go.mod --- module example.com/api go 1.23 require example.com/logger v0.0.0 replace example.com/logger => ../pkg/logger // <--- MAL
Si subes ese replace, el pipeline de CI fallará porque el runner de la CI probablemente no tiene esa estructura de carpetas o porque, al ser un módulo independiente, espera encontrar example.com/logger en un registry, no en una ruta relativa que solo existe en tu laptop.
N° 247