Vanity import path go: sacá a GitHub de tu identidad
Un ingeniero de software publicó recientemente en dev.to una guía técnica sobre cómo desacoplar código Go de GitHub usando vanity import paths. El caso que cita como advertencia: un equipo en Hanói tardó casi dos semanas en migrar 40 servicios internos después de que la empresa renombrara la organización en GitHub, según relata el artículo original.
Un vanity import path en Go es un module path que usa un dominio propio (por ejemplo go.miempresa.com/pagos) en vez de github.com o gitlab.com. Go lo resuelve haciendo una consulta HTTP a ese dominio con el parámetro ?go-get=1 y leyendo una meta tag go-import que indica dónde vive el repositorio real, un mecanismo que documenta oficialmente la referencia de cmd/go.
En este artículo:
- En 30 segundos
- ¿Qué es el module path en Go y por qué es más que un simple nombre?
- ¿Qué pasa si migrás de GitHub a GitLab sin un vanity import path?
- ¿Cómo crear un vanity import path para Go paso a paso?
- ¿Cómo configurar GOPROXY y GOPRIVATE para módulos privados de Go?
- ¿Cómo evitar que el CI y el código de negocio dependan de GitHub?
- Errores comunes al desacoplar Go de GitHub
- Preguntas Frecuentes
- Conclusión
- Fuentes
En 30 segundos
- El module path de Go funciona a la vez como identificador de import, dirección de fetch y clave en go.sum, no es solo un nombre cosmético.
- Un equipo en Hanói perdió casi dos semanas migrando 40 servicios internos tras un simple rename de organización en GitHub.
- La solución que propone la fuente es un servidor de vanity import path de apenas ~40 líneas de Go.
- El prefijo del meta tag go-import tiene que coincidir exactamente con la directiva module en go.mod, o el build falla.
- Go usa proxy.golang.org,direct como GOPROXY y sum.golang.org como GOSUMDB desde la versión 1.13, según la documentación de GitLab.
¿Qué es el module path en Go y por qué es más que un simple nombre?
El module path en Go cumple tres funciones simultáneas: es el identificador que aparece en cada sentencia import, es la dirección donde el comando go busca el código fuente cuando no hay proxy de por medio, y es la clave que se guarda en go.sum y en la base de checksums de sum.golang.org. Cuando escribís module github.com/tuempresa/pagos, atás esas tres funciones a un hostname que no controlás vos.
Ahí está el problema real. Cambiar el host no es un detalle cosmético: es crear, técnicamente, un módulo nuevo. Go no tiene ningún mecanismo de “redirect” a nivel de module path. La directiva retract y el comentario // Deprecated: en go.mod solo avisan a quien lee el archivo, no mueven a nadie automáticamente al path nuevo, según aclara la fuente original.
¿Qué pasa si migrás de GitHub a GitLab sin un vanity import path?
Pasa lo que le pasó al equipo de Hanói: casi dos semanas de trabajo para migrar unos 40 servicios internos, y la mayor parte de ese tiempo no fue escribir código nuevo sino esperar que cada equipo mergeara su PR de imports y después debuggear los mismatches de go.sum que aparecían en cadena.
Pensalo en la práctica: renombrás la organización en GitHub porque la empresa se reestructuró, y de golpe cada import github.com/organizacion-vieja/servicio en cada repositorio dependiente queda apuntando a un módulo que ya no existe con ese nombre. Nadie te avisa con un error claro y limpio. Lo que ves es un go build roto en 40 lugares distintos, cada uno con su propio checksum desincronizado. Complementá esta lectura con la guía para montar un cluster de bases de datos en bare metal.
El replace en go.mod sirve como parche de emergencia mientras hacés la migración pieza por pieza. El tema es que solo tiene efecto en el módulo principal donde lo escribís, no se propaga a quien usa tu librería como dependencia. Si tenés 40 servicios, necesitás 40 replace temporales, uno por cada uno.
¿Cómo crear un vanity import path para Go paso a paso?
Se crea con un servidor HTTP propio que responde meta tags go-import cuando Go le hace la consulta ?go-get=1. La fuente publica un ejemplo de apenas 40 líneas que cualquiera puede desplegar en un VPS, en Cloud Run o incluso como HTML estático detrás de un CDN.
Acá va una versión reducida de ese servidor:
// cmd/vanity/main.go
package main
import (
"fmt"
"log"
"net/http"
"strings"
)
const host = "go.miempresa.com"
// Solo hay que tocar este mapa cuando cambia el proveedor.
var repos = map[string]string{
"pagos": "https://github.com/miempresa/pagos",
"authkit": "https://gitlab.miempresa.com/plataforma/authkit",
}
func handler(w http.ResponseWriter, r *http.Request) {
name := strings.SplitN(strings.Trim(r.URL.Path, "/"), "/", 2)
repo, ok := repos[name]
if !ok {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Fprintf(w, `<!DOCTYPE html><html><head>
<meta name="go-import" content="%s/%s git %s">
</head><body>go get %s/%s</body></html>`, host, name, repo, host, name)
}
func main() {
http.HandleFunc("/", handler)
log.Fatal(http.ListenAndServe(":8080", nil))
}Ojo con esto: el primer campo del content (el import prefix) tiene que coincidir exactamente con la directiva module del go.mod en el repositorio real. Si no coincide, Go tira el error module declares its path as ... but was required as ... y ahí perdés media tarde buscando qué falló. El día que pagos pase de GitHub a GitLab, tocás una línea del mapa, redeployás, y ningún servicio consumidor tiene que cambiar un solo import. Más contexto en un bug de bajo nivel que rompió el rendimiento esperado.
Si vas a levantar ese servidor vos mismo, ya sea en un VPS propio o detrás de un CDN, y necesitás un dominio .com.ar para la empresa, en donweb.com lo podés registrar y alojar sin quedar atado a un proveedor de código específico.
¿Cómo configurar GOPROXY y GOPRIVATE para módulos privados de Go?
Se configuran con go env -w para que Go no envíe el nombre de tus módulos privados a proxy.golang.org ni a sum.golang.org. La fuente recomienda esta configuración para Go 1.24 en adelante:
# No mandar módulos internos al proxy y checksum DB públicos
go env -w GOPRIVATE='go.miempresa.com/*,gitlab.miempresa.com/*'
# Proxy interno primero, después fallback a proxy.golang.org
go env -w GOPROXY='https://goproxy.miempresa.com,https://proxy.golang.org,direct'
# Autenticación con token, sin hardcodear el host en el código
git config --global url."https://oauth2:${GITLAB_TOKEN}@gitlab.miempresa.com/".insteadOf "https://gitlab.miempresa.com/"Un proxy interno tipo Athens v0.15 tiene un beneficio extra que no es menor: tu build deja de depender de si GitHub está caído o rate-limiteando requests. Si alguna vez viste el CI ponerse rojo en cadena por un outage ajeno, sabés exactamente de qué se trata.
Según documenta la guía de dependencias de GitLab, desde Go 1.13 el valor por defecto de GOPROXY es proxy.golang.org,direct y el de GOSUMDB es sum.golang.org. Y desde Go 1.24, según la documentación oficial de cmd/go, existe además la variable GOAUTH para autenticar requests a proyectos privados sin depender de .netrc.
¿Cómo evitar que el CI y el código de negocio dependan de GitHub?
Se evita concentrando la lógica de build en Makefile o scripts bash, no en el YAML de GitHub Actions. Si toda tu pipeline vive en acciones de terceros dentro del workflow, el día que cambies a GitLab CI vas a tener que reescribir todo desde cero. Con un Makefile que expone make test y make release, el YAML queda como una cáscara fina que solo llama esos comandos, y lo que corre local corre igual en cualquier CI. Esto se conecta con lo que analizamos en capas de compatibilidad que ocultan la implementación real.
Lo mismo aplica a cualquier tool que llame directo a api.github.com para chequear versiones o generar issues. La fuente propone envolver esas llamadas en una interface:
type ReleaseSource interface {
Latest(ctx context.Context, project string) (Release, error)
}
// githubSource, gitlabSource y giteaSource implementan esta interface.
// El provider se elige por config, no por import fijo.Parece over-engineering para un proyecto chico, pero simplifica los tests: mockeás la interface en vez de mockear llamadas HTTP. Para el release final, GoReleaser v2 ya soporta GitHub, GitLab y Gitea; alcanza con sacar la sección release: del .goreleaser.yaml y manejarla con una variable de entorno en vez de repetir la URL del repo en distintos archivos.
| Escenario | Sin vanity import path | Con vanity import path |
|---|---|---|
| Cambio de GitHub a GitLab | Reescribís imports en cada servicio dependiente | Cambiás una línea en el mapa del servidor vanity |
| Identidad del módulo | Atada al hostname del proveedor de código | Atada a tu propio dominio |
| go.sum en migración | Se rompe en cadena en cada consumidor | No cambia porque el import prefix no cambió |
| Tiempo migrando ~40 servicios | Casi dos semanas, según el caso de Hanói | No documentado en la fuente, pero limitado a redeploy del vanity server |

Errores comunes al desacoplar Go de GitHub
- Tratar el replace como solución definitiva. Solo tiene efecto en el módulo principal, no se propaga a quien consume tu librería como dependencia transitiva.
- No hacer coincidir el import prefix con el module directive. Es la causa más común del error “module declares its path as … but was required as …”, y suele aparecer justo el día del deploy.
- Levantar el vanity server sin monitoreo. Si el dominio expira o el servidor cae, go get falla para cualquier módulo privado nuevo (las versiones ya cacheadas en proxy.golang.org siguen funcionando, pero las privadas no).
- Hardcodear llamadas a api.github.com en el tooling interno. Cualquier bot de releases o chequeo de versiones que dependa directo de esa API se rompe entero si cambiás de proveedor.
Preguntas Frecuentes
¿Qué es un vanity import path en Go?
Es un module path que usa un dominio propio en vez de github.com o gitlab.com. El comando go lo resuelve consultando ese dominio con ?go-get=1 y leyendo la meta tag go-import que apunta al repositorio real. Relacionado: depender de la infraestructura de un tercero.
¿Cómo cambiar el module path de un proyecto Go sin romper los imports?
Se agrega un module path nuevo con vanity import path y se usa la directiva replace más un comentario // Deprecated: en el go.mod viejo para migrar de forma gradual, sin forzar a todos los consumidores a actualizar el mismo día.
¿Qué son GOPROXY y GOPRIVATE en Go?
GOPROXY es la lista de proxies de módulos que Go consulta antes de ir directo al repositorio, con proxy.golang.org,direct como valor por defecto desde Go 1.13. GOPRIVATE es la lista de patrones de módulos privados que se excluyen tanto del proxy público como de la base de checksums sum.golang.org.
¿Cómo migrar un módulo de Go de GitHub a GitLab?
Con un vanity import path, migrar significa cambiar una línea en el mapa del servidor vanity que apunta el import prefix a la URL nueva del repositorio en GitLab, sin tocar ningún import en el código consumidor. Sin vanity import path, hay que reescribir cada import y regenerar go.sum en cada servicio dependiente.
¿Por qué se rompe go.sum cuando cambia el nombre del repositorio?
Porque go.sum guarda el checksum atado al module path exacto, y ese path incluye el hostname del repositorio. Si el hostname cambia (por rename de organización o migración de proveedor), Go lo trata como un módulo distinto y el checksum registrado ya no coincide con nada.
Conclusión
El dato central de esta guía es simple: el module path de Go no es solo un nombre, es una dependencia oculta hacia el proveedor de código que elegiste. El caso de Hanói (casi dos semanas migrando 40 servicios por un simple rename de organización) muestra el costo real de no haberlo pensado antes.
Si estás arrancando un proyecto nuevo, la fuente recomienda usar go mod init go.tudominio.com/nombre en vez de github.com/… desde el día uno. El costo es casi cero al principio y se vuelve caro después. Para proyectos existentes, no hace falta migrar de un día para el otro: alcanza con sumar el module path nuevo vía vanity import y usar replace más // Deprecated: para ir corriendo el tráfico de forma gradual.






