应全局复用显式配置的*http.client实例,禁用http.defaultclient和http.get;需设置maxidleconnsperhost=100、idleconntimeout=30s、tlsclientconfig启用会话缓存、proxy保留环境代理;请求须用http.newrequest构造并显式关闭resp.body。

别用 http.DefaultClient 或 http.Get
在 Fiber handler 里直接调 http.Get 或 http.DefaultClient.Do 是线上最常见性能雷区。它不设超时、不复用连接、不控制空闲连接数,压测几秒就触发 dial tcp: too many open files。Fiber 本身不封装 HTTP 客户端,但很多用户误以为“框架自带”,结果下游服务一抖,整个 goroutine 池就被卡死。
真正该做的是:全局复用一个显式配置的 *http.Client 实例,而不是每次请求都 new 一个。
- 永远不要在 handler 内部 new
&http.Client{} - 永远不要用
http.Get发生产请求——它底层调的是没设Timeout的默认 client - 所有下游调用必须走你预配置好的 client 变量,比如
app.Client或包级变量downstreamClient
必须显式配置 Transport 的四个关键参数
http.Transport 的默认值完全不适合高并发场景。Fiber 应用常跑在边缘或网关位置,下游依赖多、延迟波动大,不调这几个参数,等于裸奔。
重点不是“全设高”,而是“设对”:
-
MaxIdleConnsPerHost = 100:默认是2,10 并发就排队;设为100覆盖多数中等规模服务 -
IdleConnTimeout = 30 * time.Second:默认是0(用系统值),容易因服务端主动断连导致下次复用时重连;显式设为30s更可控 -
TLSClientConfig = &tls.Config{ClientSessionCache: tls.NewLRUClientSessionCache(100)}:HTTPS 场景必加,否则每次握手耗时翻倍 -
Proxy = http.ProxyFromEnvironment:保留本地开发或容器内代理支持,别漏掉
示例片段:
client := &http.Client{
Timeout: 5 * time.Second,
Transport: &http.Transport{
MaxIdleConnsPerHost: 100,
IdleConnTimeout: 30 * time.Second,
TLSClientConfig: &tls.Config{
ClientSessionCache: tls.NewLRUClientSessionCache(100),
},
Proxy: http.ProxyFromEnvironment,
},
}
发请求时必须手动构造 *http.Request
Fiber 的 fiber.Ctx 和标准 *http.Request 不兼容,但你要调下游 HTTP 服务,最终仍得走 net/http 栈。关键点在于:请求头(Header)、请求体(Body)、URL 路径这些元数据,只能绑定到 *http.Request 实例上,不能塞进 fiber.Ctx 或 http.Client 里。
所以三步不能少:
- 用
http.NewRequest构造请求对象(别用http.Post,它没法设 header) - 用
req.Header.Set设置Content-Type、Authorization等头字段 - 用
client.Do(req)提交,而非client.Get或http.DefaultClient
例如发 JSON:
body, _ := json.Marshal(map[string]string{"key": "value"})
req, _ := http.NewRequest("POST", "https://api.example.com/v1/data", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Request-ID", c.Locals("request_id").(string))
<p>resp, err := app.DownstreamClient.Do(req)
if err != nil { /<em> handle </em>/ }
defer resp.Body.Close() // 必须放这里,且不能跳过</p>
Body 关闭和错误处理最容易被跳过
漏关 resp.Body 不会立刻 panic,但会导致连接无法归还给连接池,几轮请求后 MaxIdleConnsPerHost 就被占满,后续所有请求排队或超时。更隐蔽的是:只在 err != nil 时关 body,或在 resp.StatusCode != 200 时提前 return,都会让 Body 永远悬着。
正确姿势只有一条:只要 client.Do 返回了 resp,不管成功失败、是否读 body,第一行就得写 defer resp.Body.Close()。
另外两个高频坑:
- 用
json.NewDecoder(resp.Body).Decode(&v)时,解码失败不等于 body 关闭了,defer仍要生效 - 如果只关心状态码,也得读完 body,哪怕只是
io.Copy(io.Discard, resp.Body),否则连接不释放 - 别在中间件里用
c.Body()后再转发给下游 client——c.Body()是单次可读流,再读就是空
连接池耗尽、TLS 握手慢、goroutine 卡住,90% 都是从这三步松动开始的。











