博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
不要使用 Go 默认的 HTTP 客户端(在生产环境中)
阅读量:2288 次
发布时间:2019-05-09

本文共 3189 字,大约阅读时间需要 10 分钟。

点击上方蓝色“Golang来啦”关注我哟

加个“星标”,天天 15 分钟,掌握 Go 语言

via:

https://medium.com/@nate510/don-t-use-go-s-default-http-client-4804cb19f779
作者:Nathan Smith
四哥水平有限,如有翻译或理解错误,烦请帮忙指出,感谢!

原文是作者 2016 发表的,点赞已经有 3K+。文章原标题是《不要使用 Go 默认的 HTTP 客户端》,发表出来之后,评论就炸锅了,按现在话说,有点标题党。一票网友认为原标题很是误导人,容易让别人以为 Go 默认的 HTTP 客户端有什么 Bug。迫于“压力”,可能作者本身也觉得标题不合适,就将标题加上了 - in production。

回过头看,文章还是非常值得推荐的,毕竟点赞数在这摆着呢。

原文如下:


通过 Go 自带的 HTTP 包写服务既简单又非常有趣。我封装过很多 API 包,发现这是一项令人愉快的任务。但是我遇到了一个很容易掉进去的坑,这个坑很容易使程序崩溃:Go 默认的 HTTP 客户端。

总的一句话:Go 默认的 HTTP 客户端没有指定请求超时时间,允许服务劫持 goroutine。当请求外部服务时,请始终使用自定义的 http.Client。

场景复现

假设你想通过 API 接口来访问 spacely-sprockets.com,获取可用的 sprocket 列表。用 Go 语言实现的话,你可能会这样做:

// error checking omitted for brevityvar sprockets SprocketsResponseresponse, _ := http.Get("spacely-sprockets.com/api/sprockets")buf, _ := ioutil.ReadAll(response.Body)json.Unmarshal(buf, &sprockets)

你将代码写好,各种 error 也都处理好,接着编译运行,一切都很美好。现在你将 API 包应用到 web 应用中,你的 app 中的某一个页面通过调用 API 向用户显示 Spacely Sprockets 的广告资源列表。

一切都运行的很平稳直到有一天你的 app 宕掉了。于是你查看日志,似乎也没找到导致宕机的原因。接着你打开了监控工具,但是 CPU、内存和 I/O 看起来都很合理,没有可能会导致宕机。最后你打开了沙箱,它工作也是正常的。那到底是什么导致宕机呢?

沮丧的是,你检查了推特,并注意到 Spacely Sprockets 开发人员团队的一条推文,说他们经历了短暂的停电,但是现在一切恢复正常。于是你查看了他们的 API 状态页,发现停电发生在 APP 宕机的前几分钟。这似乎是不太可能的巧合,但是你无法完全弄清它们之间的关系,因为你的 API 代码已经优雅地处理了各种错误。你依然无法弄清楚问题的根源在哪。

Go 的 HTTP 包

Go 的 HTTP 包使用 Client 结构体来管理 HTTP(S) 通信的内部过程。Clients 是并发安全的对象,包含配置、管理 TCP 状态、处理 cookies 等。当你使用 http.Get(url) 时,就会调用  http.DefaultClient,走的是 HTTP 默认配置,声明如下:

var DefaultClient = &Client{}

除其他配置项外,http.Client 会配置一个超时时间,当请求时间超过这个数值时,请求就会自动断开。该数值默认值是 0,即没有超时时间。该默认值对于 HTTP 包来说挺合理的,同时这的确也是一个容易让人掉进去的坑,在上面的例子中,导致了 app 崩溃。事实证明,Spacely Sprockets 的 API 中断导致我们的请求尝试挂起,进程一直 hang 住。只要发生故障的服务器没有恢复,进程就会一直挂着。因为进行 API 调用是为了服务用户请求,所以这也会导致服务用户请求的 goroutine 也挂起。一旦有足够的人点击 sprockets 页面,app 就崩溃了,很有可能是因为系统资源达到了极限。

下面是演示出现问题的 Go 示例:

package mainimport (  “fmt”  “net/http”  “net/http/httptest”  “time”)func main() {  svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {    time.Sleep(time.Hour)  }))  defer svr.Close()  fmt.Println(“making request”)  http.Get(svr.URL)  fmt.Println(“finished request”)}

编译运行程序之后,将会向服务器发送一个请求,请求发出之后会睡眠,等待一个小时之后程序正常退出。

解决办法

解决这个问题的办法就是使用 http.Client 时定义一个合理的超时时间,比如下面这样的:

var netClient = &http.Client{  Timeout: time.Second * 10,}response, _ := netClient.Get(url)

设置了 10s 的超时时间,如果超时 Get() 将会返回错误:

&httpError{  err:     err.Error() + " (Client.Timeout exceeded while awaiting headers)",  timeout: true,}

如果需要对请求生命周期进行更细粒度的控制,还可以另外指定自定义 net.Transport 和 net.Dialer。Transport 结构体用来管理底层 TCP 连接,Dialer 是用来管理连接建立的结构体。Go 的 net 包使用默认的 Transport 和 Dialer。下面是一个自定义的例子:

var netTransport = &http.Transport{  Dial: (&net.Dialer{    Timeout: 5 * time.Second,  }).Dial,  TLSHandshakeTimeout: 5 * time.Second,}var netClient = &http.Client{  Timeout: time.Second * 10,  Transport: netTransport,}response, _ := netClient.Get(url)

上面代码设置了 TCP 拨号时间、TLS 握手时间和请求超时时间。如果有需要我们还可以设置其他选项,例如 keep-alive 超时时间。

总结

Go 语言的 net/http 包是经过深思熟虑的产物,可以非常便捷地用于 HTTP(S) 通信。然而,缺少请求超时时间是一个非常容易掉进去的坑,因为这个包提供了很多诸如 http.Get(url) 等便捷的方法。其他一些语言(例如 Java)也有类似的问题;有一些开发语言则没有,比如 Ruby 就提供了 60s 的超时时间。请求远程服务时不设置超时时间会使你的应用程序依赖于该服务,如果远程服务发生故障或者有恶意程序,我们的请求将会永远挂起,从而有可能使系统资源耗尽导致宕机。

推荐阅读:

如果我的文章对你有所帮助,点赞、转发都是一种支持!

给个[在看],是对四哥最大的支持

转载地址:http://nyfnb.baihongyu.com/

你可能感兴趣的文章
DeepLearning 知识点整理
查看>>
stdcall 标准winNTdll 编写 与 调用
查看>>
Android 搭建
查看>>
Java 配置
查看>>
多个Activity的完全退出
查看>>
自定义android控件
查看>>
tomcat c/s 三层架构
查看>>
代码_多进程_简单实例
查看>>
转载_消息机制
查看>>
代码_网络_FTP
查看>>
代码_网络_WWW
查看>>
UIView常用属性和函数
查看>>
UIButton常用属性和函数详解
查看>>
UILabel常用属性详解
查看>>
UITextField常用属性和方法详解
查看>>
“UITableView完美平滑滚动”阅读笔记
查看>>
UIImageView常用属性和方法
查看>>
UIImage常用属性和方法
查看>>
会报编译器警告的Xcode 6.3新特性:Nullability Annotations
查看>>
2015 Objective-C 三大新特性
查看>>