A Real Nightmare

Last year, a friend of mine — let’s call him Alex — made a perfectly reasonable decision: migrate his company’s code from GitHub Enterprise to a self-hosted GitLab instance.

Sounds straightforward, right? Move the repos, tweak the CI, and call it a day.

Until they realized: all 200+ Go microservices had import paths starting with github.com/their-org/.

Moving the repositories took a weekend. But updating the import paths across every downstream service? Three engineers spent two weeks on it. Then another week for regression testing.

Alex told me something afterward that stuck with me:

“If we’d been using vanity URLs, all of this would have required changing a single DNS record.”

This post is for you — before you hit that wall.


Four Hidden Risks

Risk 1: Vendor Lock-in — Switching Git Hosts = Rewriting Every Import

A Go module’s import path is its identity. When you write:

import "github.com/your-org/user-service"

You’re effectively saying: this module lives on GitHub forever.

Want to migrate to GitLab? Change the imports. Want to move to Gitea? Change the imports. Self-hosted Forgejo? Still changing imports.

And it’s not just one place — it’s every project that depends on this module, every downstream service, every go.mod file. For a mid-sized team, that could mean hundreds of files modified + full regression testing.

This isn’t hypothetical. Every year, companies in the Go community go through what I call the “great import path migration” because they tied their module identity to a specific Git host.

Risk 2: IP Addresses and Internal Network Exposure

Worse than GitHub lock-in is the self-hosted Git scenario. Many companies use raw IP addresses or internal hostnames:

import "192.168.1.100/user-service"
import "gitlab.internal.corp/platform/auth"

These paths end up in:

  • Open-source project go.mod files (if accidentally made public)
  • Code screenshots, blog posts, and Stack Overflow answers
  • Former employees’ GitHub repos (never cleaned up after departure)

Your internal network topology, scattered across the internet.

Risk 3: Brand Deficit — It Doesn’t Look “Official”

This might sound superficial, but it matters in commercial contexts.

Compare these two import paths:

import "github.com/john-doe-personal-project/lib"  // 😬
import "go.yourcompany.com/lib"                     // ✨

The first looks like a personal experiment. The second looks like an officially released product.

When external customers use your SDK, the import path is your brand’s front door. github.com/xxx says “this is a GitHub repo.” go.yourcompany.com/xxx says “this is a formally released product.”

Risk 4: Management Chaos — Who Remembers What at Scale?

With 2-3 private modules, raw GitHub paths are fine. But at 10, 20, or 50 modules:

  • New hires: Is github.com/org/pkg or github.com/org/pkg-v2 the one we’re using?
  • Cross-team collaboration: How does your github.com/org/auth relate to the other team’s github.com/org2/auth-lib?
  • Module ownership: Is this repo still active? Who owns it?

Without a unified namespace, module management becomes “memory + search + luck.”


The Solution: Vanity URLs — Go’s Built-in Answer

The Go language anticipated this problem from the start. The go-import mechanism (also called Vanity URLs) was designed specifically to decouple import paths from actual code repositories.

The principle is simple: when go get requests an import path, it first visits that URL and looks for an HTML meta tag:

<meta name="go-import" content="go.yourcompany.com/lib git https://github.com/your-org/lib">

This tag tells the Go toolchain: “Even though my import path is go.yourcompany.com/lib, the actual code lives at github.com/your-org/lib.”

Then go get pulls from GitHub, but your code always uses go.yourcompany.com/lib as the import path.

This means: you can change the underlying code repository at will, and the import path never changes.

Moving to GitLab? Update the meta tag. Migrating to self-hosted servers? Update the meta tag. Splitting one repo into two? With subdirectory module support, the meta tag handles that too.

This mechanism is documented in the Go modules reference. The golang.org/x/... packages are the most well-known example — their code is hosted on Google’s own infrastructure (https://cs.opensource.google/go/x/), but the import path is golang.org/x/.


In Practice: One Command to Set Up

Knowing the theory is one thing. What about the actual setup? Hand-writing HTML meta tags, configuring HTTPS, managing DNS? That’s the old way.

gvu is a purpose-built solution for this problem — a hosted Go Vanity URL service that gets you running in one command:

# Install
curl -fsSL https://gomodvanityurls.com/install.sh | sh

# Sign up (free, no email required)
gvu auth signup

# Add your first vanity URL
gvu add gomod.io/yourname/lib https://github.com/your-org/lib

30 seconds later, your teammates can go get gomod.io/yourname/lib to pull code.

Got a custom domain? Do this:

gvu add go.yourcompany.com/auth https://github.com/your-org/auth-service

Wildcard routing, automatic major version support (/v2, /v3…), automatic HTTPS certificates — all out of the box.


Back to the Beginning

Alex’s company spent 3 weeks migrating import paths for 200 microservices. 3 engineers × 3 weeks = 9 person-weeks of cost, all because they initially thought “using raw GitHub paths is fine.”

Your import path is your code’s street address.

The street address should follow you, not your landlord.


If you’re managing Go private modules, take 30 seconds to try gvu — free gomod.io domain, free auto-HTTPS, free major version support. Your import path should be yours to control.