“What Service Is 10.0.1.50?”

Alex stared at the error message on his screen, frowning.

build failed: cannot find module "10.0.1.50/user-service":
    module lookup disabled by GOPROXY=off

He turned to the new hire sitting next to him: “Do you know what 10.0.1.50 is?”

Ben looked blank. He’d only been with the company for three days.

This was daily life at Alex’s company — a mid-sized fintech firm. The platform team had over 50 Go microservices, all using internal IP addresses as import paths. Nobody thought anything of it — until last week’s server migration.


Background: The “Primitive Era” of IP-Based Import Paths

Two years ago, when the platform team first adopted Go, everything was simple. Five microservices, code on a self-hosted GitLab, import paths using raw IPs:

import (
    "10.0.1.50/user-service"
    "10.0.1.50/order-service"
    "10.0.1.51/payment-gateway"
)

“It works,” they said. Nobody thought twice.

But two years later, microservices grew from 5 to 50+. The team expanded from 8 to 30 engineers. Problems started surfacing:

Problem 1: IP Addresses Are Unreadable

// Can you tell what each dependency does?
import (
    "10.0.1.50/user-service"    // ✅ OK
    "10.0.1.51/pay-gw"          // ❓ Payment gateway?
    "10.0.1.52/cfg-svc"         // ❓ Config service?
    "10.0.1.53/msg-queue"       // ❓ Message queue wrapper?
)

New hires had to memorize an “IP ↔ service name” mapping table on day one. Even Alex regularly confused 10.0.1.52 and 10.0.1.53.

Problem 2: Server Relocation = Import Path Nightmare

Last week, the ops team announced: the data center is moving. GitLab server IPs would change from the 10.0.1.50 range to 10.0.2.50.

Alex knew immediately this was trouble. Sure enough:

$ grep -r "10.0.1.50" --include="*.go" | wc -l
347

347 references. Spread across 50+ microservices. Every go.mod, every import statement in every source file — all needed changing.

Alex and four engineers spent an entire week:

  1. Bulk-replace import paths
  2. Update all require directives in go.mod files
  3. Update CI/CD pipeline build configs
  4. Full regression testing
  5. Fix compilation errors from “missed” references

A full week wasted on “changing addresses.”

Problem 3: Code Screenshots Leaking Internal Network Info

What scared Alex even more: one engineer posted a code screenshot on a tech forum asking for help. The screenshot clearly showed:

import "10.0.1.50/internal-auth-service"

Internal IP addresses, exposed on the public internet.

No security incident resulted, but the infosec team came knocking: “Can you use a domain name instead of bare IPs?”


Exploration: Evaluating Options

Alex started researching solutions.

Option 1: Hand-Write go-import Meta Tags

Go’s official vanity URL mechanism is simple — put a meta tag on a web page:

<meta name="go-import" content="go.company.com/user-service git http://gitlab.internal/user-service">

Theoretically possible. But 50 services meant 50 HTML pages, plus Nginx configuration, HTTPS certificates, and a routing table to maintain.

“That’s just making work for ourselves,” Alex decided.

Option 2: Point Domain at GitLab

Resolve go.company.com to the GitLab server? Doesn’t work — GitLab doesn’t know how to handle ?go-get=1 query parameters.

Option 3: Open-Source Tools

Alex found several open-source options, but they were either unmaintained or too basic to support custom domains.


Discovery: gvu

A colleague spotted gvu on Hacker News — a hosted service purpose-built for Go Vanity URLs.

Alex was skeptical at first: “Another hosted service? Can’t we self-host this?”

But the colleague said: “Just try it. It’s free. If it doesn’t work, we’ll build our own.”

30 minutes later, Alex’s opinion had done a 180.


Implementation: 50 Modules in 30 Minutes

Step 1: Install and Sign Up

$ curl -fsSL gomodvanityurls.com/install.sh | bash
$ gvu auth signup

Step 2: Bulk Add Routes

Alex wrote a simple script to batch-add all 50 microservice routes:

#!/bin/bash
SERVICES=(
  "user-service"
  "order-service"
  "payment-gateway"
  "config-service"
  "msg-queue"
  # ... 45 more
)

for svc in "${SERVICES[@]}"; do
  gvu add "go.company.com/$svc" "http://gitlab.internal/platform/$svc.git"
done

With a Pro subscription, all 50 routes were added in minutes.

Step 3: DNS Configuration

Added one CNAME record in the company DNS management console:

go.company.com → gomodvanityurls.com

Waited a few minutes for DNS propagation.

Step 4: Update Code

This was the most “painful” step — replacing all 10.0.1.50/xxx with go.company.com/xxx. But it would be the last time.

# Bulk replace
find . -name "*.go" -exec sed -i 's|10.0.1.50/|go.company.com/|g' {} +
find . -name "*.go" -exec sed -i 's|10.0.1.51/|go.company.com/|g' {} +
find . -name "*.go" -exec sed -i 's|10.0.1.52/|go.company.com/|g' {} +

Step 5: Verify

$ go get go.company.com/user-service
go: downloading go.company.com/user-service v1.2.0
go: added go.company.com/user-service v1.2.0

✅ Success.


Results: Three Months Later

New Hire Experience

Ben’s first PR now reads:

import (
    "go.company.com/user-service"
    "go.company.com/order-service"
    "go.company.com/payment-gateway"
)

No more IP lookup tables. The import path is the service name.

Server Migration

Last month, GitLab was migrated again (from physical machines to K8s). This time Alex made one change:

# Update all routes' target repos
for svc in "${SERVICES[@]}"; do
  gvu remove "$(gvu list --oneline | grep $svc | awk '{print $1}')"
  gvu add "go.company.com/$svc" "http://gitlab.k8s.internal/platform/$svc.git"
done

Zero code changes. Every microservice’s import path stayed the same. No recompilation, no CI/CD changes.

Wildcard Routing

Later, Alex discovered wildcard routing — one rule replaced all 50:

$ gvu add 'go.company.com/platform/*' 'http://gitlab.k8s.internal/platform/*.git'

New microservices are automatically importable as go.company.com/platform/xxx as long as the repo lives under the platform group. Zero configuration.


Before and After

DimensionBefore (IP addresses)After (Vanity URLs)
Readability10.0.1.50/cfg-svcgo.company.com/config-service
OnboardingIP lookup table requiredImport path = service name
Server migration347 changes, one weekUpdate DNS/routes, zero code changes
SecurityInternal IPs exposedOnly brand domain visible
BrandingLooks like an experimentLooks like a released product

Alex now recommends vanity URLs to everyone. His go-to line:

“Your import path is your code’s street address. When you move house, the street address shouldn’t change.”


If you’re managing Go private modules with IP addresses or raw GitHub paths, try gvu. 30 seconds to sign up, 30 minutes to set up vanity URLs for your entire company.