go语言http解析(一)server监听流程

今天偶然间发现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 参数的值。

  • 精确匹配符 :支持在路由末尾使用 {$}(如 /{$})来强制进行严格路径匹配,避免默认的目录前缀匹配逻辑。

go 复制代码
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的创建BindListen这几步,然后把ln传给s.Serve进行监听

rust 复制代码
http.ListenAndServe -> server.ListenAndServe
go 复制代码
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请求

go 复制代码
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)即可

go 复制代码
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()创建的,如果没有创建就使用默认的。

go 复制代码
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处理方法

scss 复制代码
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)
}

下班了,具体的解析下一篇文档再写

相关推荐
怕浪猫2 小时前
第22章:项目实战与进阶优化——从开发到部署的完整旅程
后端·go·编程语言
程序员爱钓鱼4 小时前
Go错误处理全解析:errors包实战与最佳实践
前端·后端·go
程序员爱钓鱼1 天前
Go并发控制核心:context 包完整技术解析
后端·google·go
Coding君1 天前
每日一Go-36、深入Go-CGO 深度使用--调 C 代码、跨语言交互、性能陷阱
go
我叫黑大帅2 天前
Go 语言并发编程的 “工具箱”
后端·面试·go
tyung2 天前
zhenyi-base 开源 | Go 高性能基础库:TCP 77万 QPS,无锁队列 16ns/op
后端·go
代码搬运媛2 天前
Go 语言通道 (Channel) 深度用法讲解及实战
后端·go
程序员爱钓鱼2 天前
Go生成唯一ID的标准方案:github.com/google/uuid使用详解
后端·google·go
我叫黑大帅2 天前
Go 语言中处理「未知类型数据」的两大核心手段
后端·面试·go