Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more

You can serve several domains from one Go binary by registering host-specific patterns on a single http.ServeMux. Since Go 1.22, the same pattern can also constrain the HTTP method and capture path wildcards. If a domain should be answered by a separate backend service rather than by a handler inside your process, put a net/http/httputil.ReverseProxy behind that host instead.

How host-specific patterns work

A ServeMux pattern can begin with a host name. The pattern example.com/ matches requests for example.com under any path, while api.example.com/ matches only that subdomain. A pattern with no host, such as /static/, matches every host. The port is ignored during matching, so example.com:8080 is treated as example.com when the mux chooses a handler.

This is the documented behavior in the net/http package documentation for ServeMux. The examples below follow that documented pattern syntax. They have not been compiled or run for this article, so test them against your own Go version before relying on them.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

A minimal multi-domain server

package main

import (
	"fmt"
	"net/http"
)

func siteHandler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintln(w, "marketing site")
}

func apiHandler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintln(w, "api")
}

func postHandler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "post %sn", r.PathValue("id"))
}

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("example.com/", siteHandler)
	mux.HandleFunc("api.example.com/", apiHandler)
	mux.HandleFunc("GET example.com/posts/{id}", postHandler)

	http.ListenAndServe(":8080", mux)
}

Inside a handler, read a named wildcard with r.PathValue("id"). A method prefix such as GET restricts the route. In Go 1.22 and later, GET also matches HEAD, and other methods must match exactly. When a path exists but no registered method matches, ServeMux can answer with method-not-allowed behavior instead of reaching a handler you did not intend.

Wildcards and trailing slashes

  • {name} matches one path segment.
  • {name...} must be the final segment and captures the rest of the path.
  • A trailing slash such as example.com/docs/ matches the whole subtree under /docs/.
  • {$} makes the match exact, so example.com/docs/{$} matches only the directory path itself.

Unrecognized hosts

Because a hostless pattern matches any host, adding a catch-all such as / will also answer requests for domains you did not mean to serve. If that is not what you want, register no hostless root pattern and let unmatched requests fall through to the default 404 behavior, or register an explicit handler that returns a 404 or rejects the request. Decide this deliberately for each deployment; a fallback that serves a default tenant or site is a common cause of content appearing on the wrong domain.

How ServeMux resolves overlapping patterns

ServeMux does not pick the most recently registered route. It selects the most specific matching pattern. A pattern is more specific when it matches a strict subset of the requests matched by another pattern. In the example above, GET example.com/posts/{id} is more specific than example.com/, so it wins for GET requests to /posts/….

If two patterns overlap and neither is more specific, registration fails. Handle and HandleFunc panic at startup. The one exception is a compatibility rule: a host-bearing pattern takes precedence over an otherwise-conflicting hostless pattern. Registration order does not change the result, so you can organize routes by domain without worrying about the sequence of calls.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Path cleaning and escaping

  • ServeMux cleans request paths. Paths containing dot segments or repeated slashes are redirected to a cleaned form.
  • Escaped %2e and %2f are preserved and are not treated as dot or slash separators during routing.
  • Host values are sanitized and the port is stripped for host matching.

Review these edge cases wherever canonicalization affects authorization checks, request signing, or tenant selection, because a path that is cleaned before your code sees it may differ from what a client sent.

Upgrading from Go 1.21 and earlier

The routing syntax changed in Go 1.22. Braced path segments that were literal text in Go 1.21 are treated as wildcards in Go 1.22. Some patterns that were accepted before can now panic at registration. Before adopting the new syntax, check your Go version and search existing patterns for braces.

If you need the old matching behavior temporarily, set GODEBUG=httpmuxgo121=1. The setting is read once at startup, so changing it while the process runs has no effect. Treat it as a migration aid, not a permanent setting. For the host-prefixed pattern form on an older toolchain, consult the net/http documentation for that specific release.

Direct dispatch or a reverse proxy

The right choice depends on where each domain’s requests should be answered.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Situation Recommended approach
Several domains served by handlers in the same Go process Host-specific ServeMux patterns with one handler per domain
A domain that must be forwarded to a separate backend service httputil.ReverseProxy registered on that host’s pattern
Mixed: some domains local, some forwarded One mux, with local handlers for some hosts and proxy handlers for others
Routing needs beyond what ServeMux offers, or a toolchain older than Go 1.22 A third-party router, which the Go team has said remains a fine choice for programs with advanced routing needs

The Go Blog post by Jonathan Amsterdam, written on behalf of the Go team and dated 13 February 2024, puts it this way: “But third-party web frameworks remain a fine choice for current users or programs with advanced routing needs.” The standard library is sufficient for many multi-domain servers, but it is not the only reasonable option.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Forwarding to a backend with ReverseProxy

When a domain should reach a separate service, use httputil.ReverseProxy. Its Rewrite function receives a ProxyRequest. SetURL sets the outbound scheme, host, and base path. By default it also rewrites the outbound Host header to the target host. If the backend needs the original host name, copy it back explicitly.

target, _ := url.Parse("http://shop-backend:9000")
shopProxy := &httputil.ReverseProxy{
	Rewrite: func(pr *httputil.ProxyRequest) {
		pr.SetURL(target)
		pr.Out.Host = pr.In.Host
		pr.SetXForwarded()
	},
}
mux.Handle("shop.example.com/", shopProxy)

The SetXForwarded method sets X-Forwarded-For, X-Forwarded-Host, and X-Forwarded-Proto on the outbound request. Those headers are only as trustworthy as the path the request took to reach you. If clients can connect directly to this server, or an upstream load balancer does not overwrite them, a client can supply forged values. Decide which proxies you trust, and discard forwarded headers from untrusted sources before the backend reads them.

Checklist before you deploy

  • Confirm the Go version in your go.mod and build toolchain supports the pattern syntax you use.
  • Search for existing route strings containing braces, since they may now be wildcards.
  • Decide what unknown hosts receive, and verify it with a request using an unregistered Host header.
  • Check that path canonicalization does not change any value used for authorization or tenant selection.
  • Preserve or set the outbound Host header deliberately for each reverse-proxied domain.
  • Strip client-supplied forwarded headers at the trust boundary before relying on them.

Once these checks pass, a single binary can serve many domains: local handlers handle the hosts your process owns, and proxies carry the rest to the services that own them.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.