Go 的 net/http 标准库以简单易用著称,很多开发者在使用时只知道:
http.HandleFunc("/", handler)
http.ListenAndServe(":9090", nil)
但背后的真正原理是什么?
-
请求如何到达你的 handler?
-
ServeMux 是怎么做路由匹配的?
-
自定义路由器应该怎么写?
-
ListenAndServe 内部到底经历了什么?
本篇文章将从底层来彻底讲清楚这些问题。
一、ServeMux 是什么?
ServeMux 可以理解为 Go 自带的路由器。
你平时这样写路由:
http.HandleFunc("/login", login)
实际上执行的是:
-
把
"/login"这条路由注册到 DefaultServeMux -
当发生请求时,由 DefaultServeMux 决定调用哪个 handler
所以 ServeMux 的主要职责是:
根据 URL 查找处理该请求的 Handler。
二、ServeMux 底层结构分析
源码结构:src/net/http/server.go
type ServeMux struct {
mu sync.RWMutex // 并发读写路由,需要加锁
m map[string]muxEntry // 路由表
hosts bool // 是否包含 Host 匹配规则
}
type muxEntry struct {
explicit bool // 精确匹配还是前缀匹配
h Handler // 对应的 handler
pattern string // 路由规则字符串
}
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
1. ServeMux.m:路由表
<muxEntry> 就是 ServeMux 的路由项。
你注册的每一个路由规则,如:
http.HandleFunc("/login", login)
http.HandleFunc("/", index)
都会被放入:
map[string]muxEntry {
"/": {explicit: true, h: index},
"/login": {explicit: true, h: login},
}
这个 map 就像一本“通讯录”,告诉 ServeMux:
访问某个路径时,应该调用哪一个 handler。
2. muxEntry:单条路由规则的详情
里面包含三种信息:
-
pattern(路由规则字符串)
-
handler(处理函数)
-
explicit(是否完全匹配)
比如:
pattern="/login"
explicit=true
但对于 "/"(根路径),Go 有特殊处理,它既可以精确匹配,也可以作为前缀匹配。
3. Handler 接口
所有能处理请求的对象(函数)都必须实现:
ServeHTTP(ResponseWriter, *Request)
你平时写的是:
func login(w http.ResponseWriter, r *http.Request) {}
这是普通函数。
那它怎么变成 Handler 呢?
答案是:Go 内部用 HandlerFunc 类型做了适配器。
type HandlerFunc func(ResponseWriter, *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
因此,每个函数都能被自动转成 Handler。
三、ServeMux 的工作流程(路由匹配机制)
当请求进入 ServeMux 后,它会按照以下流程处理:
1. 加锁
路由表是共享资源,因此必须保证并发安全:
mux.mu.RLock()
defer mux.mu.RUnlock()
2. 拿到 URL 路径
path := r.URL.Path
例如访问 /login
3. 在路由表中查找 handler
ServeMux 的核心匹配逻辑:
-
先找绝对匹配(explicit=true)
-
找不到则尝试最长前缀匹配
最长前缀匹配规则(非常重要):
/prefix/xxx
会匹配所有以 /prefix/ 开头的 URL。
举例:
http.HandleFunc("/static/", staticHandler)
匹配路径:
/static/a.png
/static/css/main.css
/static/js/a.js
4. 找到 muxEntry 后,调用 handler
entry.h.ServeHTTP(w, r)
也就是执行你注册的处理函数。
四、自定义路由器(实现自己的 ServeMux)
Go 的设计非常灵活,只要实现了 Handler 接口,你就可以自定义路由系统。
下面是一个最简单的自定义路由器:
type MyMux struct {}
func (p *MyMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/" {
sayhelloName(w, r)
return
}
http.NotFound(w, r)
}
func sayhelloName(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello myroute!")
}
func main() {
mux := &MyMux{}
http.ListenAndServe(":9090", mux)
}
MyMux 的运行原理
-
实现了 ServeHTTP → 等于实现了 Handler
-
http.ListenAndServe 允许我们传入自己的路由器
-
所有请求都会被 MyMux 接管
请求流程:
客户端 → MyMux.ServeHTTP → sayhelloName → 响应用户
如果你不传自己的 mux:
http.ListenAndServe(":9090", nil)
Go 会用默认的 DefaultServeMux。
五、DefaultServeMux 的完整执行链(底层执行流程)
下面我们剖析客户端发起请求后发生的所有步骤。
第一步:注册路由 http.HandleFunc
做了 3 件事:
-
调用 DefaultServeMux.HandleFunc
-
将普通函数适配为 HandlerFunc(实现了 Handler 接口)
-
将路由注册到 DefaultServeMux.m(路由表)
第二步:http.ListenAndServe(":9090", nil)
如果 handler 为 nil,则使用 DefaultServeMux。
执行顺序:
1. 创建 Server
srv := &Server{Addr:":9090", Handler:nil}
因为 Handler 为 nil → 使用 DefaultServeMux。
2. 调用 ListenAndServe()
开启 TCP 监听:
ln, err := net.Listen("tcp", ":9090")
3. 启动死循环处理连接
for {
conn, _ := ln.Accept()
go c.serve()
}
每个连接都开一个 goroutine。
第三步:处理连接 c.serve()
读取请求:
w, err := c.readRequest()
判断 handler:
handler := srv.Handler
if handler == nil {
handler = DefaultServeMux
}
调用路由器的 ServeHTTP:
handler.ServeHTTP(w, r)
也就是:
DefaultServeMux.ServeHTTP(w, r)
第四步:DefaultServeMux 选择匹配的 handler
逻辑:
-
遍历所有 muxEntry(路由规则)
-
找到最匹配的路由
-
调用该路由的 handler.ServeHTTP
最终执行的是你注册的处理函数。
六、扩展:ServeMux 的匹配规则
Go 的 ServeMux 匹配规则非常重要(尤其是 /):
1. 精确匹配优先
/login
只能匹配 /login。
2. 长度最长的前缀匹配
/static/ → 匹配 /static/*
/api/ → 匹配 /api/*
/ → 匹配所有路径
因此 / 会成为兜底路由。
3. Host 匹配规则
路由可以带 host:
http.HandleFunc("www.example.com/login", handler)
但一般开发中几乎不用。
4. 并发安全
ServeMux 使用 RWMutex 保证对路由表读写安全:
-
多读并行
-
写时独占
七、总结:Go Web 路由工作全流程
下面是整个过程的完整链路图
Browser
↓
TCP connection
↓
Server.Accept()
↓
Conn.serve()
↓
readRequest()
↓
找到 handler(默认是 DefaultServeMux)
↓
DefaultServeMux.ServeHTTP()
↓
路由匹配 muxEntry
↓
调用 handler.ServeHTTP()
↓
你的处理函数执行
↓
ResponseWriter 写回客户端
Go语言ServeMux路由原理解析
211

被折叠的 条评论
为什么被折叠?



