问题一: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需要被关闭