Revel控制器中直接用http.Client发请求会阻塞主线程,因其默认同步模型将所有逻辑运行于单goroutine;正确做法是剥离HTTP调用、显式配置超时与重试、复用client并关闭resp.Body。

Revel控制器里直接用http.Client发请求会卡住主线程
Revel默认是同步处理模型,所有逻辑跑在同一个 goroutine 里。如果你在 Controller.Action 方法中直接用 http.DefaultClient.Do() 调第三方接口,一旦那个接口响应慢或超时,整个请求就会阻塞,后续并发请求全排队——T99 延迟可能飙升到秒级甚至更久。
真正该做的是:把 HTTP 请求从 Controller 主流程里剥离出来,要么用 goroutine 包一层(注意错误传递和资源回收),要么改用异步客户端(如 http.Client 配合 context.WithTimeout)。
- 别用
http.Get或http.Post这类快捷函数,它们不支持超时控制 - 必须显式构造
http.Client,并设置Timeout字段(它同时控制连接+读取,但不够精细) - 如果目标服务不稳定,仅靠
Timeout不够,得自己加重试逻辑,因为 Revel 没内置 HTTP 重试中间件
超时要分三段设:连接、读取、总耗时
只设一个 http.Client.Timeout 是常见误区。它等价于“连接超时 + 读取超时”之和,无法单独调控。而真实线上问题往往出在某一段:比如连接很快建好了,但服务端卡在 DB 查询上迟迟不返回 body——这时 Timeout 会等到最后才炸,浪费等待时间。
正确做法是用 http.Transport 拆开配置:
client := &http.Client{
Timeout: 10 * time.Second, // 总兜底
Transport: &http.Transport{
DialContext: (&net.Dialer{
Timeout: 3 * time.Second, // 连接建立超时
KeepAlive: 30 * time.Second,
}).DialContext,
TLSHandshakeTimeout: 3 * time.Second, // TLS 握手超时
ResponseHeaderTimeout: 5 * time.Second, // header 返回超时(即 socket 超时)
ExpectContinueTimeout: 1 * time.Second,
},
}-
ResponseHeaderTimeout是最关键的:它管的是“TCP 连接已建立,但服务端迟迟不发响应头”的场景 -
Timeout作为最后一道保险,防止整个请求生命周期失控 - Revel 的
Controller本身没有请求级 context,所以你要手动传context.WithTimeout给client.Do(req.WithContext(ctx))
重试不能无脑循环,得按错误类型分级
Revel 不提供声明式重试,你得自己写逻辑。但不是所有失败都适合重试:400 Bad Request、401 Unauthorized、403 Forbidden 这类客户端错误重试没意义,反而加重对方负担。
只对这几类错误重试才合理:
- 网络层错误:
net.OpError(连接拒绝、超时、无路由等) - HTTP 状态码:
5xx(尤其是502、503、504) - 部分
429 Too Many Requests(需配合 Retry-After header)
示例片段(放在 Controller 方法内):
var resp *http.Response
var err error
for i := 0; i < 3; i++ {
req, _ := http.NewRequest("GET", "https://api.example.com/data", nil)
ctx, cancel := context.WithTimeout(c.Request.Context(), 8*time.Second)
defer cancel()
req = req.WithContext(ctx)
<pre class="brush:php;toolbar:false;">resp, err = client.Do(req)
if err == nil && resp.StatusCode/100 == 2 {
break // 成功
}
if !shouldRetry(err, resp) {
break // 不该重试,跳出
}
if i < 2 {
time.Sleep(time.Second * time.Duration(1<<i)) // 指数退避
}}
shouldRetry 函数要检查 err 类型和 resp.StatusCode,别漏掉 resp == nil 的情况(说明请求根本没发出去)。
别忘了连接池和资源释放
Revel 默认不复用 http.Client,每次 new 一个就开新连接池,容易触发 "too many open files"。你应该把 client 声明为包级变量或注入到 Controller 中,而不是在 Action 里临时创建。
- 全局复用一个
*http.Client实例,它的Transport天然支持连接复用 - 务必调用
resp.Body.Close(),否则底层 TCP 连接不会归还给连接池,长连接数会持续增长 - 如果用了自定义
http.Transport,记得设MaxIdleConns和MaxIdleConnsPerHost,否则默认值(100/1000)在高并发下可能不够
最易被忽略的一点:Revel 的 c.Request 是 *http.Request,但它绑定的 context 生命周期只到 Action 返回为止。你在 goroutine 里用它发请求,必须用 c.Request.Context() 派生新 context,并设好超时——否则 goroutine 可能永远挂在那里。


















