http.ServeMux无法用于二级域名分发,因其只匹配URL路径而不解析Host头;必须自定义Handler提取Host或:authority头,统一转小写后按子域规则路由,并注意端口剥离、国家域名、代理差异及上下文传递。

用 http.ServeMux 做二级域名分发会失败
Go 标准库的 http.ServeMux 只匹配请求路径(Request.URL.Path),完全不看 Host 头。你写 mux.Handle("blog.example.com/", ...) 是无效的——它根本不会解析 Host,更不会做前缀或通配匹配。
必须自己拦截 http.Handler,从 Request.Host 或 Request.URL.Host 提取子域,再手动路由。
- 注意区分
localhost:8080和真实域名:开发时 Host 可能带端口,上线后通常不带,需用strings.SplitN(r.Host, ":", 2)安全切分 - 泛解析场景下,
api.example.com、shop.example.com都该走同一套逻辑,但www.example.com可能要单独处理 - 别直接用
r.Host == "blog.example.com"判等——HTTP/2 可能传小写 Host,且用户可能输大写,建议统一转小写再比对
用 net/http 手写 Host 分发器的核心写法
最轻量的做法是包一层 http.Handler,在 ServeHTTP 里解析 Host 并委托给不同子 handler:
func NewSubdomainRouter() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
host := r.Host
if idx := strings.Index(host, ":"); idx != -1 {
host = host[:idx] // 去掉端口
}
parts := strings.Split(host, ".")
if len(parts) < 3 {
http.Error(w, "Bad Host", http.StatusBadRequest)
return
}
subdomain := parts[0]
switch subdomain {
case "api":
apiHandler.ServeHTTP(w, r)
case "blog":
blogHandler.ServeHTTP(w, r)
case "shop":
shopHandler.ServeHTTP(w, r)
default:
// 泛解析兜底:所有其他子域都进 mainHandler
mainHandler.ServeHTTP(w, r)
}
})
}
- 别忘了处理 HTTPS 重定向:如果后端只跑 HTTP,但前端有反向代理(如 Nginx)转发了
X-Forwarded-Proto,就得靠这个头判断是否应跳转 - 泛解析不能只靠
len(parts) >= 3:像test.co.uk这类国家域名实际二级域是co.uk,但 Go 默认不识别——简单项目可忽略,高要求需引入golang.org/x/net/publicsuffix - 子域提取逻辑和业务强耦合:比如
v2.api.example.com要归到 api 分支,就得改成从右往左数,取倒数第三段开始拼接
泛解析时 Host 和 Authority 的差异陷阱
HTTP/2 的 Authority 伪头(r.Header.Get(":authority"))可能和 r.Host 不一致,尤其在代理链路中。Nginx 默认不透传 :authority,而 Caddy 会传;如果你的应用直连客户端,两者通常一样;但加了网关后,r.Host 可能被覆盖成网关地址,:authority 才是原始请求目标。
立即学习“go语言免费学习笔记(深入)”;
- 生产环境务必以
:authority为准:先查这个头,不存在再 fallback 到r.Host - 不要信任
r.URL.Host:它由 Go 自动解析,可能已被中间件篡改或未同步更新,优先读原始 header - 调试时用
fmt.Printf("Host=%q, Authority=%q\n", r.Host, r.Header.Get(":authority"))对比两值,能快速定位代理是否丢头
用 gorilla/mux 或 chi 做子域路由的注意事项
第三方路由库虽支持 Host 匹配,但泛解析不是开箱即用的功能。比如 gorilla/mux 的 Host 方法只支持固定字符串或正则,写 Host("{subdomain:[a-z]+}.example.com") 看似可行,但 {subdomain} 捕获的变量不会自动注入 handler 的 map[string]string,得手动从 mux.Vars(r) 取,而且正则无法表达“任意非空子域”这种语义(.+ 会误吞点号)。
-
chi更干净:它原生支持Subrouter+ 中间件提取 Host,推荐用chi.Context存子域名,后续 handler 直接取 - 无论用哪个库,都别把泛解析逻辑塞进路由定义里——复杂正则难维护,出错难 debug,不如前置中间件统一提取并写入 context
- 泛解析路由一旦启用,就无法再用
http.FileServer直接挂静态资源:因为FileServer不知道你从 context 里拿了啥子域,必须包装一层 handler 显式传参
泛解析真正麻烦的不是怎么写,而是怎么让每个 handler 都意识到“当前请求属于哪个子域”,以及当子域嵌套(如 staging.api.example.com)或含短横线(my-app.example.com)时,提取规则是否健壮。这些边界情况,上线前必须拿真实 DNS 解析结果测一遍。











