golang http使用踩过的坑与填坑指南


Posted in Golang onApril 27, 2021

golang对http进行了很好的封装, 使我们在开发基于http服务的时候, 十分的方便, 但是良好的封装, 很容易是的我们忽略掉它们底层的实现细节。

如下是我踩过的一些坑, 以及相应的解决方法。

调用http服务

通常的实践如下:

resp, err := http.Get("http://example.com/")
if err != nil {
               // handle error
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
// ...

陷阱一: Response body没有及时关闭

网络程序运行中, 过了一段时间, 比较常见的问题就是爆出错误:“socket: too many open files”, 这通常是由于打开的文件句柄没有关闭造成的。

在http使用中, 最容易让人忽视的, 就是http返回的response的body必须close,否则就会有内存泄露。

更不容易发现的问题是, 如果response.body的内容没有被读出来, 会造成socket链接泄露, 后续的服务无法使用。

这里, response.body是一个io.ReadCloser类型的接口, 包含了read和close接口。

type Response struct { 
    // Body represents the response body.
    //
    // The response body is streamed on demand as the Body field
    // is read. If the network connection fails or the server
    // terminates the response, Body.Read calls return an error.
    //
    // The http Client and Transport guarantee that Body is always
    // non-nil, even on responses without a body or responses with
    // a zero-length body. It is the caller's responsibility to
    // close Body. The default HTTP client's Transport may not
    // reuse HTTP/1.x "keep-alive" TCP connections if the Body is
    // not read to completion and closed.
    //
    // The Body is automatically dechunked if the server replied
    // with a "chunked" Transfer-Encoding.
    Body io.ReadCloser
 }

如果没有通过ioutil.ReadAll或者其他的接口读取response.body的内容, 此次socket链接就无法被后续的连接复用, 造成的结果就是该连接一直存在。

尽管调用了ioutil.ReadAll就可以避免该连接的泄露, 我们还是建议在获取response后, 就调用Close, 因为在response返回的地方与ReadAll之间, 万一有条件判断造成接口提前返回, 还是会造成泄露的。

defer resp.Body.Close()

另外, http.Request是不需要主动关闭的。

陷阱二: 默认的http的transport的设定不合适

在简单的应用下, 采用默认的http client就可以满足需要, 在稍微复杂一点的场景, 有其实想要保持长链接以及提高链接复用的效率等方面的控制, 这个时候就需要对client比较清楚的了解。

type Client struct {
    // Transport specifies the mechanism by which individual
    // HTTP requests are made.
    // If nil, DefaultTransport is used.
    Transport RoundTripper  
    // Timeout specifies a time limit for requests made by this
    // Client. The timeout includes connection time, any
    // redirects, and reading the response body. The timer remains
    // running after Get, Head, Post, or Do return and will
    // interrupt reading of the Response.Body.
    //
    // A Timeout of zero means no timeout.
    //
    // The Client cancels requests to the underlying Transport
    // as if the Request's Context ended.
    //
    // For compatibility, the Client will also use the deprecated
    // CancelRequest method on Transport if found. New
    // RoundTripper implementations should use the Request's Context
    // for cancelation instead of implementing CancelRequest.
    Timeout time.Duration
}

这里, 我们重点关注Transport与Timeout两个字段, Transport记录了本次请求的事务信息, 以及连接复用相关的信息。

Timeout记录此次调用的超时时间以避免异常发生的时候的长时间等待。

通常我们使用的默认的Transport定义如下:

var DefaultTransport RoundTripper = &Transport{
    Proxy: ProxyFromEnvironment,
    DialContext: (&net.Dialer{
        Timeout:   30 * time.Second,
        KeepAlive: 30 * time.Second,
        DualStack: true,
    }).DialContext,
    MaxIdleConns:          100,
    IdleConnTimeout:       90 * time.Second,
    TLSHandshakeTimeout:   10 * time.Second,
    ExpectContinueTimeout: 1 * time.Second,
}

默认情况下, 它会保留打开的连接以备未来复用, 如果服务要连接很多的主机, 就会保存很多的空闲连接, IdleConnTimeout用来将超过一定时间的空闲连接回收;实际上, Defaulttransport 的MaxIdleConns是100, 在很多的场景下还是偏小的, 尤其是对于需要管理大的系统并且模块之间交互频繁的情况。

另外, 如果该连接需要定期 访问很多的资源节点, 并列我们知道每个资源节点上面需要的连接数大于2, 那么就会出现很多的短连接, 因为对于每一台资源机, DefaultTransport默认的最大连接数是2, 最大空闲连接是1.

type Transport struct {
     // MaxIdleConnsPerHost, if non-zero, controls the maximum idle
    // (keep-alive) connections to keep per-host. If zero,
    // DefaultMaxIdleConnsPerHost is used.
    MaxIdleConnsPerHost int
    
    // MaxConnsPerHost optionally limits the total number of
    // connections per host, including connections in the dialing,
    // active, and idle states. On limit violation, dials will block.
    //
    // Zero means no limit.
    //
    // For HTTP/2, this currently only controls the number of new
    // connections being created at a time, instead of the total
    // number. In practice, hosts using HTTP/2 only have about one
    // idle connection, though.
    MaxConnsPerHost int
}

HTTP的长连接与TCP的长连接

在http1.1中, http默认保持长连接, 以备将来复用, 但是这个长连接通常是有时间限制的, 并且向我们上面开到的Transport里面的设定, 空闲的连接数是有最大限制的, 超过了该限制,其余新的连接就变成了短连接。

TCP协议本身是长连接, 它超过一定时间没有数据传送, 就会发送心跳来检测该连接是否存活, 如果是, 该连接继续有效。

补充:golang 设置 http response 响应头的内容与坑

用 golang 写 http server 时,可以很方便可通过 w.Header.Set(k, v) 来设置 http response 中 header 的内容。

例如:w.Header().Set("Access-Control-Allow-Origin", "*") 。

但是需要特别注意的是某些时候不仅要修改 http header ,还要修改 http status code。

修改 http status code 可以通过:w.WriteHeader(code) 来实现,例如:w.WriteHeader(404) 。

如果这两种修改一起做,就必须让 w.WriteHeader 在所有的 w.Header.Set 之后,也就是 w.WriteHeader 后 Set Header 是无效的。

今天就遇到了这个问题,在一段代码中调用 w.Header.Set,怎么折腾都无效,最后才发现其它代码段中先调用了 w.WriteHeader。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持三水点靠木。如有错误或未考虑完全的地方,望不吝赐教。

Golang 相关文章推荐
Go缓冲channel和非缓冲channel的区别说明
Apr 25 Golang
goland 设置project gopath的操作
May 06 Golang
Golang的继承模拟实例
Jun 30 Golang
Go语言实现一个简单的并发聊天室的项目实战
Mar 18 Golang
详解Golang如何优雅的终止一个服务
Mar 21 Golang
简单聊聊Golang中defer预计算参数
Mar 25 Golang
Go语言 详解net的tcp服务
Apr 14 Golang
Golang jwt身份认证
Apr 20 Golang
Go获取两个时区的时间差
Apr 20 Golang
Golang 实现 WebSockets 之创建 WebSockets
Apr 24 Golang
Golang 入门 之url 包
May 04 Golang
基于Python实现西西成语接龙小助手
Aug 05 Golang
Golang 实现超大文件读取的两种方法
Apr 27 #Golang
golang中的空slice案例
Apr 27 #Golang
Go语言切片前或中间插入项与内置copy()函数详解
golang中切片copy复制和等号复制的区别介绍
Apr 27 #Golang
go语言中切片与内存复制 memcpy 的实现操作
Apr 27 #Golang
Go语言中的UTF-8实现
Apr 26 #Golang
golang中实现给gif、png、jpeg图片添加文字水印
Apr 26 #Golang
You might like
重量级动漫纷纷停播!唯独OVERLORD第四季正在英魂之刃继续更新
2020/05/06 日漫
PHP 第二节 数据类型之转换
2012/04/28 PHP
php计算几分钟前、几小时前、几天前的几个函数、类分享
2014/04/09 PHP
PHP中时间加减函数strtotime用法分析
2017/04/26 PHP
Javascript 篱式条件判断
2008/08/22 Javascript
javascript实现window.print()去除页眉页脚
2014/12/30 Javascript
解析ajaxFileUpload 异步上传文件简单使用
2016/12/30 Javascript
详解JS对象封装的常用方式
2016/12/30 Javascript
nodejs利用http模块实现银行卡所属银行查询和骚扰电话验证示例
2016/12/30 NodeJs
jQuery的三种bind/One/Live/On事件绑定使用方法
2017/02/23 Javascript
Bootstrap 过渡效果Transition 模态框(Modal)
2017/03/17 Javascript
JavaScript中在光标处插入添加文本标签节点的详细方法
2017/03/22 Javascript
深入理解vue2.0路由如何配置问题
2017/07/18 Javascript
利用pm2部署多个node.js项目的配置教程
2017/10/22 Javascript
解决vue2.0 element-ui中el-upload的before-upload方法返回false时submit()不生效问题
2018/08/24 Javascript
axios使用拦截器统一处理所有的http请求的方法
2018/11/02 Javascript
如何进行微信公众号开发的本地调试的方法
2019/06/16 Javascript
layui表格分页 记录勾选的实例
2019/09/02 Javascript
详解BootStrap表单验证中重置BootStrap-select验证提示不清除的坑
2019/09/17 Javascript
vue实现数据控制视图的原理解析
2020/01/07 Javascript
解决vue-cli输入命令vue ui没效果的问题
2020/11/17 Javascript
[02:38]DOTA2英雄基础教程 噬魂鬼
2014/01/03 DOTA
浅谈python中列表、字符串、字典的常用操作
2017/09/19 Python
Python告诉你木马程序的键盘记录原理
2019/02/02 Python
wxPython实现画图板
2020/08/27 Python
html5 css3网站菜单实现代码
2013/12/23 HTML / CSS
HMV日本官网:全球知名的音乐、DVD和电脑游戏零售巨头
2016/08/13 全球购物
远东集团网络工程师面试题
2014/10/20 面试题
银行存款证明样本
2014/01/17 职场文书
小学生学习雷锋倡议书
2014/05/15 职场文书
2014年村支部书记四风对照检查材料思想汇报
2014/10/02 职场文书
幼儿园大班见习报告
2014/10/31 职场文书
2014年仓库保管员工作总结
2014/12/03 职场文书
大学生个人简历自我评价
2015/03/11 职场文书
Spring Boot 启动、停止、重启、状态脚本
2021/06/26 Java/Android
mysql查询结果实现多列拼接查询
2022/04/03 MySQL