http.ListenAndServe 启动流程为:创建 http.Server 实例并调用其 ListenAndServe 方法,内部通过 net.Listen 获取 Listener,再由 Server.Serve 循环 Accept 连接并为每个连接启动 goroutine 执行 serverHandler.ServeHTTP。

从 http.ListenAndServe 入口开始跟踪最简 HTTP 服务启动流程
Go 的 net/http 启动逻辑藏得不深,但容易被 http.Serve 和底层 net.Listener 的抽象绕晕。直接从最常用的 http.ListenAndServe 看起,它本质是两步:创建 http.Server 实例 + 调用其 ListenAndServe 方法。
关键点在于:http.ListenAndServe 默认使用全局变量 http.DefaultServeMux 作为路由复用器,而真正监听和接受连接的是 net.Listen("tcp", addr) 返回的 net.Listener。这个 Listener 会被传给 Server.Serve,后者进入一个死循环调用 Accept()。
-
Server.Serve不解析 HTTP 报文,只负责接收连接、起 goroutine 调用serverHandler.ServeHTTP - 每个连接对应一个独立 goroutine,所以并发模型天然由 Go runtime 支撑,无需手动管理线程池
- 如果传入自定义
http.Server实例,务必注意ReadTimeout/WriteTimeout(已弃用)或更推荐的ReadHeaderTimeout、IdleTimeout,否则连接可能长期 hang 住
理解 ServeHTTP 接口如何串联 Handler 链
所有能处理 HTTP 请求的类型,都必须实现 http.Handler 接口——也就是只有一个方法:ServeHTTP(http.ResponseWriter, *http.Request)。这是整个请求分发的核心契约。
http.ServeMux 是最典型的 Handler 实现,它内部维护一个 map[string]muxEntry,把路径前缀映射到具体 handler;而 http.HandlerFunc 是个类型别名,靠闭包转成函数值后,再通过强制转换满足接口要求。
立即学习“go语言免费学习笔记(深入)”;
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 自定义中间件本质就是返回一个新的
http.Handler,比如func(h http.Handler) http.Handler { return loggingHandler{h} } -
http.StripPrefix和http.FileServer组合时,要注意路径匹配顺序:StripPrefix 先截掉前缀,再交给 FileServer 解析剩余路径,否则静态文件 404 - 不要在
ServeHTTP中 panic,除非你启用了http.Server.ErrorLog并做了 recover 包装,否则 panic 会直接 kill goroutine 导致连接无声断开
为什么 http.Request.Body 只能读一次?源码里怎么做的
Request.Body 是一个 io.ReadCloser,底层通常是 io.LimitedReader 包裹的网络连接缓冲区。它的“一次性”行为不是约定,而是由 readLimitBody(在 server.go 中)和 bodyEOFSignal 这两个结构体硬编码决定的。
一旦调用 req.Body.Read() 或 io.ReadAll(req.Body),底层 reader 的 offset 就往前走了;再次 Read 就从新位置开始,而网络连接早已关闭或丢弃了旧数据。
- 想多次读 body,必须在第一次读完后用
req.Body = io.NopCloser(bytes.NewReader(data))重置 - 用
req.ParseForm()或req.FormValue()也会隐式读 body(对 POST 表单),所以它们和显式读 body 不能混用,否则第二次读为空 - 生产环境建议统一用
io.Copy(ioutil.Discard, req.Body)快速丢弃未处理 body,避免连接被卡在 read 状态
调试时怎么快速定位到 http.Transport 的超时控制点
客户端侧的超时最容易混淆,因为 http.Client 本身没有“总超时”,而是拆成三段:连接建立(DialContext)、TLS 握手(TLSHandshakeTimeout)、响应头读取(ResponseHeaderTimeout)。这些全在 http.Transport 字段里配置。
关键线索在 transport.go 的 RoundTrip 方法里:它先调用 getConn 获取连接(触发 Dial 和 TLS),再调用 writeHeaders,最后用 readResponse 读响应头——每一步都有自己的 context deadline。
-
Client.Timeout只作用于整个RoundTrip调用,但它不会中断正在进行的底层 I/O,仅影响上层 context cancel - 若服务端迟迟不发响应头,
ResponseHeaderTimeout才真正切断连接;没设它,哪怕Client.Timeout到了,连接也可能卡在readLoop里 - 调试时可在
transport.go的roundTrip开头加日志,或用godebug在getConn和readResponse处打断点,比看文档更快定位阻塞点
server.go 顶部开始逐行扫,先盯住几个核心函数: ListenAndServe、ServeHTTP、readRequest、writeChunk、RoundTrip。它们分散在不同文件,但构成了请求生命周期的骨架。细节如 header 解析、chunked 编码、keep-alive 管理,都是围绕这几条主线展开的。

















