Why Not Uppercase in Go Modules Name?

I work with Go modules a lot at work. Over time, I've developed a habit of quickly checking the latest version of a module by visiting the /@latest endpoint of a Go proxy, like this:

❯ curl https://proxy.golang.org/github.com/aws/aws-sdk-go/@latest | jq
{
  "Version": "v1.55.6",
  "Time": "2025-01-15T18:57:15Z",
  "Origin": {
    "VCS": "git",
    "URL": "https://github.com/aws/aws-sdk-go",
    "Hash": "e1db430efbf87c6fd64a01c3330ad7df794b8847",
    "Ref": "refs/tags/v1.55.6"
  }
}

This trick is particularly useful when you're building tooling that relies on fetching the latest version of a Go dependency.

Similarly, you can get a list of all available versions using the /@v/list endpoint:

❯ curl https://proxy.golang.org/github.com/aws/aws-sdk-go/@v/list
v1.16.1
v1.13.54
v1.13.9
v1.50.20
# ... (omitted for brevity)

One day, I noticed a strange uptick in errors popping up in our Go proxy server log:

invalid module path encoding "git.company.net/dbops/Redis/v8"

Not a real example

The Investigation

Puzzled, I figured I’d check the Go module repository git.company.net/dbops/Redis/v8 directly, and it worked just fine! Everything resolved with a lovely 200 OK response, which left me scratching my head.

Now even more confused, I decided to double-check by hitting the Go proxy endpoint directly to see if it was actually giving me errors from the client-side:

❯ curl -I https://goproxy.company.net/git.company.net/dbops/Redis/v8/@v/list
HTTP/2 500
date: Fri, 07 Feb 2025 00:04:15 GMT
content-type: application/json; charset=utf-8
cache-control: no-cache, no-store, must-revalidate

And indeed, it was. I started wondering if there was a bug with the Go proxy server!

Running go get with -x flag

Next on my agenda was to try running the go get command on that specific module path with the -x flag.

Now, I expected the go get command to fail too.

💡
Use the -x flag to print the underlying commands as they are executed. This is useful for debugging version control commands when a module is downloaded directly from a repository.
go get -x git.company.net/dbops/Redis/[email protected]
# get https://goproxy.company.net/github.com/dgryski/go-rendezvous/@v/list
# get https://goproxy.company.net/github.com/cespare/xxhash/v2/@v/list
# get https://goproxy.company.net/git.company.net/dbops/%21redis/v8/@v/list
# get https://goproxy.company.net/github.com/dgryski/go-rendezvous/@v/list: 200 OK (0.513s)
# get https://goproxy.company.net/github.com/dgryski/go-rendezvous/@latest
# get https://goproxy.company.net/github.com/cespare/xxhash/v2/@v/list: 200 OK (0.536s)
# get https://goproxy.company.net/github.com/dgryski/go-rendezvous/@latest: 200 OK (0.385s)
# get https://goproxy.company.net/git.company.net/dbops/%21redis/v8/@v/list: 200 OK (1.645s)
go: added github.com/cespare/xxhash/v2 v2.1.2
go: added github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f
go: added git.company.net/dbops/Redis/v8 v8.9.10

But guess what? It didn’t fail!

On closer look at the requests sent by the go get command, I noticed the characters %21 — that’s when it hit me that it is the URL-encoded version of the ! character!

# get https://goproxy.company.net/git.company.net/dbops/%21redis/v8/@v/list
# get https://goproxy.company.net/git.company.net/dbops/%21redis/v8/@v/list: 200 OK (1.645s)

If you decode the URL it's git.company.net/dbops/!redis/v8/@v/list

So, why was the module name being encoded?

How Go Handles Module

So, it turns out, Go proxy server (e.g. official proxy.golang.org and Athens), converts uppercase letters in module by by prefixing them with a bang (!) followed by a lowercase.

💬
I eventually found the reason why Go does this in the Go documentation.

TL;DR due to the case-insensitive nature of some file systems & web servers, Go needed a way to distinguish between modules with the same name but different casing.

For example:

  • github.com/Azure/azure-sdk-for-go becomes github.com/!azure/azure-sdk-for-go
  • github.com/GoogleCloudPlatform/cloudsql-proxy becomes github.com/!google!cloud!platform/cloudsql-proxy

Oh, go commands automatically converts it to lowercase and URL-encodes any special characters. In my case, Redis became !redis (which is %21redis when URL-encoded).

This explains why hitting the proxy endpoint directly was giving me errors while my go get command didn’t.

Closing Thoughts

The lesson here? It's best to avoid uppercase letters in module names, especially if you intend to build tooling around Go proxy. Stick to lowercase, and save yourself from some head-scratching moments.

References

Written by human. Hosted on Digital Ocean.
ссс