When you want to distribute Go packages, the standard approach is to host them on platforms like GitHub, GitLab, or Bitbucket. Alongside that, Go also provides a powerful mechanism that allows you to host packages under your own domain while keeping the actual code in any version control system. This approach, known as ✨vanity import paths✨, gives you control over your package’s import path and branding.
For example, instead of importing a package as github.com/ariefrahmansyah/pkg, I can use ariefrahmansyah.com/pkg. This provides flexibility where I can change the underlying repository location without breaking existing code that imports my package.
In this deep dive, we’ll explore how Go’s import path discovery mechanism works, how to implement custom package hosting, and best practices for maintaining your own Go package domain.
When you run go get ariefrahmansyah.com/pkg, Go doesn’t immediately know where to find the code. Instead, it follows a discovery process that involves querying the domain and looking for specific HTML meta tags.
The implementation of this discovery mechanism is part of the Go toolchain itself and you can find the relevant code in the Go source repository:
cmd/go/internal/vcs - Contains the logic for parsing go-import and go-source meta tagscmd/go/internal/vcs/vcs.go - Handles HTTP requests with the ?go-get=1 query parameterThis mechanism is built into the go get command, so no additional configuration is needed on the client side where Go automatically handles the discovery process.
Here’s what happens when Go encounters a custom import path:
sequenceDiagram
participant Developer
participant GoTool as go get
participant DomainServer
participant VCS as Version Control<br/>(GitHub/GitLab/etc)
Developer->>GoTool: go get example.com/pkg
GoTool->>DomainServer: HTTP GET https://example.com/pkg?go-get=1
DomainServer-->>GoTool: HTML with go-import meta tag
GoTool->>GoTool: Parse meta tag<br/>extract VCS type and repo URL
GoTool->>VCS: Clone repository from extracted URL
VCS-->>GoTool: Package source code
GoTool-->>Developer: Package installed
The key insight is that Go makes an HTTP request to the domain with a special query parameter ?go-get=1. The server must respond with an HTML page containing specific meta tags that tell Go where to find the actual source code.
The go-import meta tag is the heart of custom package hosting. It has the following format:
<meta name="go-import" content="import-prefix vcs repo-root" />
The content attribute contains three space-separated parts:
ariefrahmansyah.com/pkg)git, svn, hg, or bzr)For example:
<meta
name="go-import"
content="ariefrahmansyah.com/pkg git https://github.com/ariefrahmansyah/pkg"
/>
This tells Go that:
ariefrahmansyah.com/pkghttps://github.com/ariefrahmansyah/pkgWhile go-import is required for package discovery, go-source is optional but highly recommended. It provides links to view the source code, directory listings, and individual files. This enhances the developer experience when browsing your package on pkg.go.dev.
The format is:
<meta name="go-source" content="import-prefix home directory file" />
The content attribute contains four space-separated parts:
go-import{/dir} placeholder){/dir}, {file}, and {line} placeholders)For example:
<meta
name="go-source"
content="ariefrahmansyah.com/pkg https://github.com/ariefrahmansyah/pkg https://github.com/ariefrahmansyah/pkg/tree/main{/dir} https://github.com/ariefrahmansyah/pkg/blob/main{/dir}/{file}#L{line}"
/>
This enables:
Now that we understand how it works, let’s implement custom package hosting for your domain.
Before you begin, ensure you have:
The simplest approach is to create an HTML page that serves the required meta tags. If you’re using a static site generator like Astro (as in this site), you can create a dedicated page.
Looking at the implementation in this site, I have a GoLayout component that handles the meta tags:
// src/layouts/GoLayout.astro
---
import BaseHead from "@/components/BaseHead.astro";
import "@/styles/global.css";
const { title, description, tags, goImport, goSource } = Astro.props;
---
<!doctype html>
<html lang="en">
<head>
<BaseHead title={title} description={description} tags={tags} />
<meta name="go-import" content={goImport} />
<meta name="go-source" content={goSource} />
</head>
<body>
<slot />
</body>
</html>
And a page that uses this layout where I can set the meta tags:
// src/pages/pkg.astro
---
import GoLayout from "@/layouts/GoLayout.astro";
---
<GoLayout
title="ariefrahmansyah.com/pkg"
description="ariefrahmansyah.com/pkg"
goImport="ariefrahmansyah.com/pkg git https://github.com/ariefrahmansyah/pkg"
goSource="ariefrahmansyah.com/pkg https://github.com/ariefrahmansyah/pkg https://github.com/ariefrahmansyah/pkg/tree/main{/dir} https://github.com/ariefrahmansyah/pkg/blob/main{/dir}/{file}#L{line}"
>
<p>ariefrahmansyah.com/pkg</p>
</GoLayout>
Key Points:
ariefrahmansyah.com/pkg, the page should be at /pkg (or /pkg/ with redirect)?go-get=1 to the URL, but your page should work without itIf you’re not using a static site generator, here another approach:
Create a static HTML file and serve it:
<!DOCTYPE html>
<html>
<head>
<meta
name="go-import"
content="example.com/pkg git https://github.com/username/pkg"
/>
<meta
name="go-source"
content="example.com/pkg https://github.com/username/pkg https://github.com/username/pkg/tree/main{/dir} https://github.com/username/pkg/blob/main{/dir}/{file}#L{line}"
/>
<meta
http-equiv="refresh"
content="0; url=https://github.com/username/pkg"
/>
</head>
<body>
<p>
Redirecting to
<a href="https://github.com/username/pkg">package repository</a>...
</p>
</body>
</html>
If you’re using Nginx, you can serve the meta tags directly:
location /pkg {
add_header Content-Type text/html;
return 200 '<html><head><meta name="go-import" content="example.com/pkg git https://github.com/username/pkg"></head></html>';
}
You can also create a simple Go HTTP server:
package main
import (
"fmt"
"net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
html := `<!DOCTYPE html>
<html>
<head>
<meta name="go-import" content="example.com/pkg git https://github.com/username/pkg">
<meta name="go-source" content="example.com/pkg https://github.com/username/pkg https://github.com/username/pkg/tree/main{/dir} https://github.com/username/pkg/blob/main{/dir}/{file}#L{line}">
</head>
<body>
<p>example.com/pkg</p>
</body>
</html>`
w.Header().Set("Content-Type", "text/html")
fmt.Fprint(w, html)
}
func main() {
http.HandleFunc("/pkg", handler)
http.ListenAndServe(":80", nil)
}
After deploying your page, test the configuration:
# Test that the meta tags are served correctly
curl "https://ariefrahmansyah.com/pkg?go-get=1" | grep go-import
# Test that Go can discover and download the package
go get ariefrahmansyah.com/pkg
# Verify the package is in your module cache
go list -m -versions ariefrahmansyah.com/pkg
If everything is configured correctly, go get should successfully download your package.
Once set up, others can use your package just like any other Go package:
package main
import (
"fmt"
"ariefrahmansyah.com/pkg/cache"
)
func main() {
c := cache.NewSimpleCache[string, string]()
c.Set("key", "value")
value, err := c.Get("key")
if err != nil {
log.Fatalf("Failed to get value: %v", err)
}
fmt.Println(value)
}
Go modules support semantic versioning. When using custom import paths, versioning works the same way:
# Get a specific version
go get ariefrahmansyah.com/[email protected]
# Get the latest version
go get ariefrahmansyah.com/pkg@latest
# Update to latest
go get -u ariefrahmansyah.com/pkg
Your repository should follow semantic versioning with Git tags (e.g., v0.1.0, v1.0.0, v1.2.3).
You can host multiple packages under the same domain by creating separate pages/routes for each:
example.com/pkg1 → Points to github.com/username/pkg1example.com/pkg2 → Points to github.com/username/pkg2example.com/utils/strings → Points to github.com/username/utils/stringsEach package needs its own page with the appropriate go-import meta tag.
You can also host packages in subpaths:
<!-- For example.com/utils/strings -->
<meta
name="go-import"
content="example.com/utils git https://github.com/username/utils"
/>
When someone imports example.com/utils/strings, Go will:
example.com/utils?go-get=1strings subdirectory in that repositoryHTTPS Required: Always use HTTPS. Go will warn users if they try to fetch packages over HTTP.
Domain Control: Only set up custom import paths for domains you control. Never use someone else’s domain without permission.
Repository Integrity: Go modules use checksums to verify package integrity. The Go checksum database (sum.golang.org) will automatically index your package once it’s publicly available.
Redirects: Be careful with redirects. Go follows redirects, but complex redirect chains can cause issues.
Always include go-source: This improves the developer experience on pkg.go.dev and makes your package more discoverable.
Use semantic versioning: Tag your releases properly so users can pin to specific versions.
Keep it simple: The HTML page doesn’t need to be fancy. A simple page with the meta tags is sufficient.
Document your package: Write good Go documentation. It will appear on pkg.go.dev automatically.
Test thoroughly: Test your setup with go get before announcing your package.
Consider automation: For multiple packages, consider using tools like govanityurls to manage vanity URLs automatically.
Many popular Go projects use custom import paths:
golang.org/x/tools → Points to go.googlesource.com/toolsk8s.io/client-go → Points to github.com/kubernetes/client-gogopkg.in/yaml.v2 → Points to github.com/go-yaml/yamlUse custom hosting when:
Stick with standard hosting when:
Hosting custom Go packages on your own domain provides flexibility, branding control, and independence from specific hosting providers. The mechanism is elegant. It only requires a few HTML meta tags to enable Go’s tooling to discover and fetch your packages seamlessly.
The key takeaways:
?go-get=1 to discover packagesgo-import meta tag tells Go where to find your repositorygo-source meta tag enhances documentation and browsingWhether you’re building a personal library or a suite of enterprise packages, custom import paths give you the control you need while maintaining compatibility with Go’s ecosystem.