golang处理http response碰到的问题和需要注意的点
问题一:ioutil.ReadAll(resp.Body)之后再次读取,还会读到响应体的Body内容吗?
问题二:resp.Body为何需要人为的手动关闭?
ioutil.ReadAll(resp.Body)之后再次读取,还会读到响应体的Body内容吗?
在处理http response的时候,我们会发现body读取之后想再次读取的时候,发现读不到任何东西,示例代码如下:
package main
import (
"net/http"
"crypto/tls"
"time"
"fmt"
"io/ioutil"
"os"
)
var (
URL="http://www.baidu.com"
filePath="myhttptest.go"
tr = &http.Transport{
TLSClientConfig:&tls.Config{InsecureSkipVerify:true},
}
client = &http.Client{
Transport:tr,
Timeout:10*time.Second,
}
)
func main() {
readHTML(URL)
readFile(filePath)
}
// 测试读取网页,2次读取响应体
func readHTML(url string) {
req,_:=http.NewRequest(http.MethodGet,url,nil)
resp,err:=client.Do(req)
if err!=nil{
fmt.Println("请求失败",err)
}
defer resp.Body.Close() // 必须人为手动关闭,
respBody,_:=ioutil.ReadAll(resp.Body)
fmt.Println("第一次读取响应体respBody:",string(respBody)) // 返回首页html页面
respBody2,_:=ioutil.ReadAll(resp.Body) // 第二次读取,即使不关闭resp.Body也会读取不到数据
fmt.Println("第二次读取响应体respBody:",string(respBody2)) // 返回空
}
// 测试读取文件内容,2次读取文件内容
func readFile(path string) {
file,err:=os.Open(path)
if err!=nil{
panic(err)
}
defer file.Close()
byte1,err:=ioutil.ReadAll(file)
fmt.Println("第一次读取文件内容:",string(byte1))
byte2,err:=ioutil.ReadAll(file)
fmt.Println("第二次读取文件内容:",string(byte2))
}
通过上面的代码中的2种示例(读取网页响应体,读取文件内容),我们发现无论读取的是网页的响应体还是文件内容,当我们再次读取的时候,已经读取不到内容了。所以我们猜测应该是ioutil.ReadAll()是有记录偏移量,所以会出现第二次读取不到的情况。作为client端处理response的时候会碰到这个问题,作为server端要处理request body的时候,一样会遇到此问题,那么该如何解决这个问题呢?
我们去看resp.Body源代码,发现resp,Body的类型是io.ReadCloser,
// http客户端(Client)和传输(Transport)保证响应体总是非空的,即使响应没有响应体或0长响应
// 体。关闭响应体是调用者的责任。默认http客户端传输(Transport)不会尝试复用keep-alive的
// http/1.0、http/1.1连接,除非请求体已被完全读出而且被关闭了。
Body io.ReadCloser
// ReadCloser is the interface that groups the basic Read and Close methods.
type ReadCloser interface {
Reader
Closer
}
于是有一个方法就是再造一个io.ReadCloser,如下:
// 测试读取文件内容,2次读取文件内容
func readFile(path string) {
file,err:=os.Open(path)
if err!=nil{
panic(err)
}
defer file.Close()
byte1,err:=ioutil.ReadAll(file)
fmt.Println("第一次读取文件内容:",string(byte1))
byte2,err:=ioutil.ReadAll(file)
fmt.Println("第二次读取文件内容:",string(byte2))
// 再造一个io.ReadCloser可以实现第二次还读取到文件内容
file2:=ioutil.NopCloser(bytes.NewBuffer(byte1))
byte3,_:=ioutil.ReadAll(file2)
fmt.Println("第二次读取文件内容:",string(byte3))
}
resp.Body为何需要人为的手动关闭?
作为client端处理response的时候,有一点要注意的是,body一定要手动close,否则会造成GC回收不到,继而产生内存泄露。其实在go的官方源码注释中,也明确注明了response body需要调用方法进行手动关闭【It is the caller`s responsibility to close Body:关闭是调用者的责任】,至于response body为什么需要进行关闭
那么作为client端生成的request body,需不需要手动关闭呢,答案是不需要的,net/http中的func (c *Client) Do(req *Request) (*Response, error)会调用Close()。
同样的,作为server端接收的request body,也是需要关闭,由Server自动进行关闭【The Server will close the request body. The ServeHTTP Handler does not need to:服务器将关闭请求正文。 ServeHTTP处理程序不需要。】
更多详细内容请参考:为什么Response.Body需要被关闭