今天偶然间发现go的net/http库好像在1.22版本更新了新的路由匹配方式,但是网上搜了一下没有看到相关文章,那就自己研究一下输出一份文档吧(本文使用go 1.25.0)
go1.22+ 新语法
-
基于 HTTP 方法的匹配:现在可以直接在路由模式中指定 HTTP 方法。例如,
http.HandleFunc("POST /items", handler)声明该路由仅匹配 POST 请求。 -
URL 路径通配符:
- 单段通配符:例如
/items/{id},匹配单个 URL 片段。 - 多段通配符:例如
/files/{path...},匹配剩余的多个层级路径。
- 单段通配符:例如
-
Request.PathValue:配合通配符功能,http.Request新增了PathValue方法。在处理函数中可以通过r.PathValue("id")直接提取 URL 参数的值。 -
精确匹配符:支持在路由末尾使用
{$}(如/{$})来强制进行严格路径匹配,避免默认的目录前缀匹配逻辑。
package main
import (
"fmt"
"log"
"net/http"
"time"
)
// 1. 定义一个中间件:它接收一个 Handler,返回一个新的 Handler
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
log.Printf("➡️ [中间件-入] 开始处理请求: %s %s", r.Method, r.URL.Path)
// 将请求传递给下一个 Handler(也就是我们的实际业务逻辑)
next.ServeHTTP(w, r)
log.Printf("⬅️ [中间件-出] 请求处理完毕,耗时: %v", time.Since(start))
})
}
// 2. 定义核心业务逻辑 Handler
func helloHandler(w http.ResponseWriter, r *http.Request) {
// 获取 Go 1.22+ 原生支持的路径参数 {name}
name := r.PathValue("name")
if name == "" {
name = "Guest"
}
fmt.Fprintf(w, "Hello, %s! Welcome to Go 1.25 net/http!", name)
}
func main() {
mux := http.NewServeMux()
// 3. 注册路由,使用 Go 1.22+ 的新语法 "METHOD /path"
// 我们用 loggingMiddleware 把基础的 helloHandler 包装了起来
mux.Handle("GET /hello/{name}", loggingMiddleware(http.HandlerFunc(helloHandler)))
log.Println("服务器启动在 :8080 端口...")
// 4. 启动服务
if err := http.ListenAndServe(":8080", mux); err != nil {
log.Fatal(err)
}
}
库函数
我们查看一下现在net/http库都有哪些函数 go doc net/http | grep "^func"
| 分类 | 函数名 | 简要描述 |
|---|---|---|
| 客户端请求发起 (Client Requests) | Get | 发起 HTTP GET 请求。 |
Head | 发起 HTTP HEAD 请求,通常用于仅获取响应头而不获取响应体。 | |
Post | 发起 HTTP POST 请求,需指定 Content-Type。 | |
PostForm | 发起带有 URL 编码表单数据的 HTTP POST 请求 (application/x-www-form-urlencoded)。 | |
NewRequest | 构造一个新的 HTTP 请求实例 (*Request),不会发送请求,通常用于配合自定义 Client 使用。 | |
NewRequestWithContext | 构造一个带有 Context 的新 HTTP 请求,方便进行超时控制和请求取消。 | |
| 服务端监听与路由 (Server & Routing) | ListenAndServe | 监听 TCP 网络地址,并调用 handler 处理传入的 HTTP 连接。 |
ListenAndServeTLS | 监听 TCP 网络地址,并启动提供 HTTPS 服务的服务端(需要证书和私钥)。 | |
Serve | 接收 net.Listener 上的底层网络连接,为每个连接启动对应的 HTTP 读写协程。 | |
ServeTLS | 接收 net.Listener 上的底层网络连接,并启动 HTTPS 服务。 | |
Handle | 将一个实现了 Handler 接口的对象注册到指定的 HTTP 路由模式(路径)上。 | |
HandleFunc | 将一个普通函数(需满足特定签名)注册到指定的 HTTP 路由模式上,是最常用的路由绑定方式。 | |
| 服务端响应辅助 (Response Helpers) | Error | 快速向客户端返回指定的错误信息文本和 HTTP 状态码。 |
NotFound | 快速向客户端返回 HTTP 404 Not Found 错误。 | |
Redirect | 将客户端请求重定向到指定的 URL,并返回相应的 3xx 状态码。 | |
ServeContent | 使用 io.ReadSeeker 的内容回复请求。它能处理 Range 请求(断点续传)并自动推断 Content-Type。 | |
ServeFile | 读取指定的本地文件或目录的内容,并将其作为 HTTP 响应返回。 | |
ServeFileFS | 与 ServeFile 类似,但从指定的 fs.FS 文件系统中读取文件内容(非常适合 Go 1.16+ 的内嵌文件系统)。 | |
| Cookie 处理 (Cookies) | ParseCookie | 解析 HTTP 请求头中的 Cookie 字符串,返回包含多个 *Cookie 的切片。 |
ParseSetCookie | 解析 HTTP 响应头中的 Set-Cookie 字符串,返回单一的 *Cookie。 | |
SetCookie | 在给定的 ResponseWriter 中添加 Set-Cookie 响应头。 | |
| 代理配置 (Proxy Setup) | ProxyFromEnvironment | 检查环境变量(如 HTTP_PROXY, HTTPS_PROXY)并返回应使用的代理 URL。 |
ProxyURL | 返回一个代理函数,该函数会无条件地返回配置好的固定代理 URL。 | |
| 解析、格式化与底层 (Parsers & Utils) | CanonicalHeaderKey | 返回 HTTP 头的规范化格式(例如将 "accept-encoding" 转换为 "Accept-Encoding")。 |
DetectContentType | 通过读取数据的前最多 512 字节(魔数)来推测其 MIME Content-Type。 | |
MaxBytesReader | 包装底层的 io.ReadCloser,限制请求体的最大读取字节数,常用于防止恶意的大体积请求引发内存耗尽。 | |
ParseHTTPVersion | 解析 HTTP 版本字符串(如 "HTTP/1.1"),返回主版本号和次版本号。 | |
ParseTime | 尝试用 HTTP/1.1 规范中允许的三种时间格式来解析时间字符串。 | |
ReadRequest | 从底层的 bufio.Reader 中读取并解析出一个完整的 HTTP 请求对象(主要用于底层网络编程或自定义代理)。 | |
ReadResponse | 从底层的 bufio.Reader 中读取并解析出一个完整的 HTTP 响应对象。 | |
StatusText | 根据 HTTP 状态码(如 200, 404)返回其对应的标准英文描述(如 "OK", "Not Found")。 |
net/http 相关结构
我们使用命令go doc net/http | grep "^type"|grep struct 查看都有哪些模块
| 结构体 (Struct) | 分类 | 作用简述 (Description) |
|---|---|---|
Client | 客户端 | HTTP 客户端的顶层入口。负责发送请求并接收响应,内部管理着超时设置(Timeout)、重定向策略(CheckRedirect)以及 Cookie 容器(Jar)。 |
Transport | 客户端 | Client 的底层引擎。负责真正的网络通信细节,管理 TCP 连接池复用(Keep-Alive)、代理配置、TLS 握手以及 HTTP/2 连接等。 |
Server | 服务端 | HTTP 服务的顶层实例。用于配置和启动服务端,包含监听地址(Addr)、处理程序(Handler)、以及各类读写超时(ReadTimeout/WriteTimeout)的设定。 |
ServeMux | 服务端/路由 | HTTP 请求的多路复用器(即“路由器”)。负责将接收到的 Request 的 URL 与开发者注册的路由模式进行匹配,并调用对应的 Handler。 |
Request | 请求与响应 | 代表一个 HTTP 请求的完整上下文。在客户端,它代表准备发送的数据;在服务端,它代表已接收到的数据。包含 Method、URL、Header、Body 等。 |
Response | 请求与响应 | 代表一个 HTTP 响应。在客户端,它代表接收到的服务端返回;在服务端内部(通常作为 http.ResponseWriter 接口存在),用于构建要发送的内容。 |
ResponseController | 服务端控制 | (Go 1.20 引入)更高级的响应控制器。它解耦了原有的 ResponseWriter,允许你在处理函数中针对单个请求执行高级操作,如刷新缓冲区(Flush)、设置读写截止时间(Deadlines)或劫持底层 TCP 连接(Hijack)。 |
Cookie | 数据载体 | 代表一个 HTTP Cookie。既可以从请求头中解析出来,也可以构建后写入响应头的 Set-Cookie 中。 |
CrossOriginProtection | 安全配置 | (Go 1.25 引入)跨源保护配置对象。用于配置基于 Fetch Metadata 的现代 CSRF(跨站请求伪造)防护机制。 |
HTTP2Config | 协议配置 | 用于精细化调整 HTTP/2 协议行为的配置选项(例如并发流限制),可应用于 Client 的 Transport 或 Server。 |
Protocols | 协议配置 | 用于配置服务器或客户端支持并首选的 ALPN 协议(如控制是否开启或强制使用 HTTP/1.1 或 HTTP/2)。 |
PushOptions | 协议配置 | 以前用于配置 HTTP/2 服务端推送(Server Push)行为的选项。(注:随着现代浏览器逐渐放弃 Server Push,此功能在实践中已较少使用)。 |
MaxBytesError | 错误类型 | 当使用 http.MaxBytesReader 限制请求体大小,且客户端上传的字节数超过设定阈值时,触发此特定错误。 |
ProtocolError | 错误类型 | 当解析 HTTP 请求或响应时,遇到违反 HTTP 协议规范的情况(如畸形的 Header)时返回的错误。 |
http server 接收流程
从ListenAndServe进入,ListenAndServe主要获取一个tcp的listen,用于监听外部请求,Listen里面完成了tcp socket的创建、Bind、Listen这几步,然后把ln传给s.Serve进行监听
http.ListenAndServe -> server.ListenAndServe
func (s *Server) ListenAndServe() error {
ln, err := net.Listen("tcp", addr)
if err != nil {
return err
}
return s.Serve(ln)
}
Serve函数主要就是监听请求,然后调用l.Accept()完成连接,之后调用go c.serve(connCtx)真正去处理http请求
func (s *Server) Serve(l net.Listener) error {
// 保留原始 listener,供 BaseContext 等回调感知真实的监听对象。
origListener := l
// 包一层,确保 Close 最多执行一次,避免重复关闭底层 listener。
l = &onceCloseListener{Listener: l}
defer l.Close()
// 在开始 accept 循环前完成 HTTP/2 相关初始化。
if err := s.setupHTTP2_Serve(); err != nil {
return err
}
// 将当前 listener 注册到 Server 中;如果服务已关闭,则直接返回。
if !s.trackListener(&l, true) {
return ErrServerClosed
}
defer s.trackListener(&l, false)
// baseCtx 是本次 Serve 生命周期内所有连接上下文的根 context。
baseCtx := context.Background()
if s.BaseContext != nil {
baseCtx = s.BaseContext(origListener)
if baseCtx == nil {
panic("BaseContext returned a nil context")
}
}
var tempDelay time.Duration // how long to sleep on accept failure
// 把当前 Server 放进上下文,后续连接处理链路可以取到它。
ctx := context.WithValue(baseCtx, ServerContextKey, s)
for {
// 主循环持续接受新连接;每次成功 accept 后会派生一个新的服务 goroutine。
rw, err := l.Accept()
if err != nil {
// 如果服务正在关闭,accept 失败视为正常结束。
if s.shuttingDown() {
return ErrServerClosed
}
// 临时性网络错误采用指数退避重试,避免短时异常导致 Serve 退出。
if ne, ok := err.(net.Error); ok && ne.Temporary() {
if tempDelay == 0 {
tempDelay = 5 * time.Millisecond
} else {
tempDelay *= 2
}
if max := 1 * time.Second; tempDelay > max {
tempDelay = max
}
s.logf("http: Accept error: %v; retrying in %v", err, tempDelay)
time.Sleep(tempDelay)
continue
}
// 非临时错误直接返回,终止整个 Serve 循环。
return err
}
// 以 server 级上下文为基础,为当前连接构造上下文。
connCtx := ctx
if cc := s.ConnContext; cc != nil {
connCtx = cc(connCtx, rw)
if connCtx == nil {
panic("ConnContext returned nil")
}
}
// accept 成功后清空退避时间,后续临时错误重新从最小延迟开始。
tempDelay = 0
// 为底层连接构造 serverConn,并在单独 goroutine 中处理该连接的请求生命周期。
c := s.newConn(rw)
c.setState(c.rwc, StateNew, runHooks) // before Serve can return
go c.serve(connCtx)
}
}
c.serve(connCtx)里面主要是http2升级、tls解析之类的操作,我们关注无需关注,我们聚焦http解析的主流程 serverHandler{c.server}.ServeHTTP(w, w.req)即可
func (c *conn) serve(ctx context.Context) {
// 记录远端地址,后续日志和错误输出会用到。
if ra := c.rwc.RemoteAddr(); ra != nil {
c.remoteAddr = ra.String()
}
// 把本地监听地址放进上下文,供请求处理链路读取。
ctx = context.WithValue(ctx, LocalAddrContextKey, c.rwc.LocalAddr())
var inFlightResponse *response
defer func() {
// 兜底捕获 handler panic,避免整个 server 进程被单个连接拖垮。
if err := recover(); err != nil && err != ErrAbortHandler {
const size = 64 << 10
buf := make([]byte, size)
buf = buf[:runtime.Stack(buf, false)]
c.server.logf("http: panic serving %v: %v\n%s", c.remoteAddr, err, buf)
}
// 如果当前还有未完成响应,退出前撤销它的上下文并关闭 100-continue 通知。
if inFlightResponse != nil {
inFlightResponse.cancelCtx()
inFlightResponse.disableWriteContinue()
}
// 被 hijack 的连接已经转交给调用方管理,这里不再由 net/http 关闭。
if !c.hijacked() {
if inFlightResponse != nil {
inFlightResponse.conn.r.abortPendingRead()
inFlightResponse.reqBody.Close()
}
c.close()
c.setState(c.rwc, StateClosed, runHooks)
}
}()
// HTTPS 连接先做 TLS 握手;如果 ALPN 协商到其他协议(例如 HTTP/2),会转交对应处理器。
if tlsConn, ok := c.rwc.(*tls.Conn); ok {
tlsTO := c.server.tlsHandshakeTimeout()
if tlsTO > 0 {
dl := time.Now().Add(tlsTO)
c.rwc.SetReadDeadline(dl)
c.rwc.SetWriteDeadline(dl)
}
if err := tlsConn.HandshakeContext(ctx); err != nil {
// If the handshake failed due to the client not speaking
// TLS, assume they're speaking plaintext HTTP and write a
// 400 response on the TLS conn's underlying net.Conn.
var reason string
if re, ok := err.(tls.RecordHeaderError); ok && re.Conn != nil && tlsRecordHeaderLooksLikeHTTP(re.RecordHeader) {
io.WriteString(re.Conn, "HTTP/1.0 400 Bad Request\r\n\r\nClient sent an HTTP request to an HTTPS server.\n")
re.Conn.Close()
reason = "client sent an HTTP request to an HTTPS server"
} else {
reason = err.Error()
}
c.server.logf("http: TLS handshake error from %s: %v", c.rwc.RemoteAddr(), reason)
return
}
// Restore Conn-level deadlines.
if tlsTO > 0 {
c.rwc.SetReadDeadline(time.Time{})
c.rwc.SetWriteDeadline(time.Time{})
}
c.tlsState = new(tls.ConnectionState)
*c.tlsState = tlsConn.ConnectionState()
if proto := c.tlsState.NegotiatedProtocol; validNextProto(proto) {
if fn := c.server.TLSNextProto[proto]; fn != nil {
h := initALPNRequest{ctx, tlsConn, serverHandler{c.server}}
// Mark freshly created HTTP/2 as active and prevent any server state hooks
// from being run on these connections. This prevents closeIdleConns from
// closing such connections. See issue https://golang.org/issue/39776.
c.setState(c.rwc, StateActive, skipHooks)
fn(c.server, tlsConn, h)
}
return
}
}
// HTTP/1.x from here on.
// 某些连接虽然不是 *tls.Conn,但仍能暴露 TLS 状态;这里补齐 Request.TLS 所需信息。
// Set Request.TLS if the conn is not a *tls.Conn, but implements ConnectionState.
if c.tlsState == nil {
if tc, ok := c.rwc.(connectionStater); ok {
c.tlsState = new(tls.ConnectionState)
*c.tlsState = tc.ConnectionState()
}
}
// 连接级 context:连接关闭、请求结束或 server 终止时都会触发取消。
ctx, cancelCtx := context.WithCancel(ctx)
c.cancelCtx = cancelCtx
defer cancelCtx()
// 初始化该连接的读写包装器,后续 HTTP/1 请求都会复用这些缓冲区。
c.r = &connReader{conn: c, rwc: c.rwc}
c.bufr = newBufioReader(c.r)
c.bufw = newBufioWriterSize(checkConnErrorWriter{c}, 4<<10)
protos := c.server.protocols()
// 明文连接也可能升级为 h2c;若成功切走 HTTP/2,这个函数就结束了。
if c.tlsState == nil && protos.UnencryptedHTTP2() {
if c.maybeServeUnencryptedHTTP2(ctx) {
return
}
}
if !protos.HTTP1() {
return
}
// HTTP/1 keep-alive 主循环:在同一条 TCP 连接上持续读取并处理多个请求。
for {
w, err := c.readRequest(ctx)
if c.r.remain != c.server.initialReadLimitSize() {
// If we read any bytes off the wire, we're active.
c.setState(c.rwc, StateActive, runHooks)
}
// server 进入 shutdown 后,不再接收该连接上的新请求。
if c.server.shuttingDown() {
return
}
if err != nil {
// 请求读取失败时,根据错误类型决定返回哪种 HTTP 响应,或直接断开连接。
const errorHeaders = "\r\nContent-Type: text/plain; charset=utf-8\r\nConnection: close\r\n\r\n"
switch {
case err == errTooLarge:
// Their HTTP client may or may not be
// able to read this if we're
// responding to them and hanging up
// while they're still writing their
// request. Undefined behavior.
const publicErr = "431 Request Header Fields Too Large"
fmt.Fprintf(c.rwc, "HTTP/1.1 "+publicErr+errorHeaders+publicErr)
c.closeWriteAndWait()
return
case isUnsupportedTEError(err):
// Respond as per RFC 7230 Section 3.3.1 which says,
// A server that receives a request message with a
// transfer coding it does not understand SHOULD
// respond with 501 (Unimplemented).
code := StatusNotImplemented
// We purposefully aren't echoing back the transfer-encoding's value,
// so as to mitigate the risk of cross side scripting by an attacker.
fmt.Fprintf(c.rwc, "HTTP/1.1 %d %s%sUnsupported transfer encoding", code, StatusText(code), errorHeaders)
return
case isCommonNetReadError(err):
return // don't reply
default:
if v, ok := err.(statusError); ok {
fmt.Fprintf(c.rwc, "HTTP/1.1 %d %s: %s%s%d %s: %s", v.code, StatusText(v.code), v.text, errorHeaders, v.code, StatusText(v.code), v.text)
return
}
const publicErr = "400 Bad Request"
fmt.Fprintf(c.rwc, "HTTP/1.1 "+publicErr+errorHeaders+publicErr)
return
}
}
// Expect 100 Continue support
req := w.req
if req.expectsContinue() {
// 只有 handler 真正开始读 Body 时,才回 100 Continue。
if req.ProtoAtLeast(1, 1) && req.ContentLength != 0 {
// Wrap the Body reader with one that replies on the connection
req.Body = &expectContinueReader{readCloser: req.Body, resp: w}
w.canWriteContinue.Store(true)
}
} else if req.Header.get("Expect") != "" {
w.sendExpectationFailed()
return
}
c.curReq.Store(w)
// 如果请求体还没读完,就在 EOF 后启动后台读,帮助探测客户端是否继续发数据。
if requestBodyRemains(req.Body) {
registerOnHitEOF(req.Body, w.conn.r.startBackgroundRead)
} else {
w.conn.r.startBackgroundRead()
}
// HTTP cannot have multiple simultaneous active requests.[*]
// Until the server replies to this request, it can't read another,
// so we might as well run the handler in this goroutine.
// [*] Not strictly true: HTTP pipelining. We could let them all process
// in parallel even if their responses need to be serialized.
// But we're not going to implement HTTP pipelining because it
// was never deployed in the wild and the answer is HTTP/2.
inFlightResponse = w
// HTTP/1 同一连接上的请求按顺序处理,所以直接在当前 goroutine 里跑 handler。
serverHandler{c.server}.ServeHTTP(w, w.req)
inFlightResponse = nil
w.cancelCtx()
if c.hijacked() {
c.r.releaseConn()
return
}
// 收尾并把响应缓冲刷出;之后决定连接是否还能复用。
w.finishRequest()
c.rwc.SetWriteDeadline(time.Time{})
if !w.shouldReuseConnection() {
if w.requestBodyLimitHit || w.closedRequestBodyEarly() {
c.closeWriteAndWait()
}
return
}
// 处理完一个请求后连接进入 idle,等待同一客户端发送下一个请求。
c.setState(c.rwc, StateIdle, runHooks)
c.curReq.Store(nil)
if !w.conn.server.doKeepAlives() {
// We're in shutdown mode. We might've replied
// to the user without "Connection: close" and
// they might think they can send another
// request, but such is life with HTTP/1.1.
return
}
if d := c.server.idleTimeout(); d > 0 {
c.rwc.SetReadDeadline(time.Now().Add(d))
} else {
c.rwc.SetReadDeadline(time.Time{})
}
// 在真正读下一个请求前先等连接再次变为可读,避免过早开始计算下一轮超时。
// Wait for the connection to become readable again before trying to
// read the next request. This prevents a ReadHeaderTimeout or
// ReadTimeout from starting until the first bytes of the next request
// have been received.
if _, err := c.bufr.Peek(4); err != nil {
return
}
c.rwc.SetReadDeadline(time.Time{})
}
}
这里的handler就是我们在示例代码中mux := http.NewServeMux()创建的,如果没有创建就使用默认的。
func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
handler := sh.srv.Handler
if handler == nil {
handler = DefaultServeMux
}
if !sh.srv.DisableGeneralOptionsHandler && req.RequestURI == "*" && req.Method == "OPTIONS" {
handler = globalOptionsHandler{}
}
handler.ServeHTTP(rw, req)
}
接下来我们看handler.ServeHTTP(rw, req),其中use121代表go 1.21及其之前的版本,后面的mux.findHandler就是go 1.22新加入的路由树,下面的h.ServeHTTP(w, r)就是我们注册进来的http handler处理方法
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
var h Handler
if use121 {
// 兼容旧版 1.21 的 mux 匹配逻辑。
h, _ = mux.mux121.findHandler(r)
} else {
// 选择最匹配当前请求的 handler,并把匹配结果记录到请求对象上,
// 供后续代码读取 Pattern、通配匹配值等信息。
h, r.Pattern, r.pat, r.matches = mux.findHandler(r)
}
// 路由分发完成后,把请求交给最终匹配到的 handler 处理。
h.ServeHTTP(w, r)
}
下班了,具体的解析下一篇文档再写