Golang中如何读取已关闭的net/http.Response:自定义API客户端实现
Golang中如何读取已关闭的net/http.Response:自定义API客户端实现 你好,
我正在学习Go语言,在尝试第二次读取net/http.Response.Body时遇到了一个问题。我创建了下面的示例程序来解释我正在尝试做的事情。
该示例程序从https://jsonplaceholder.typicode.com/posts发起请求,并将响应中的数据格式化为一个简单的结构体图。此操作成功执行后,response.Body会被关闭。此外,原始的net/http.response结构体被存储在结构体图中,以便客户端开发者可以访问原始的请求和头信息等。
我遇到的问题是在单元测试中,我第二次读取response.Body以压缩JSON响应,以便与预期的JSON响应进行比较。然而,我发现这会引发一个错误,大概是因为我之前在将HTTP响应体反序列化到结构体图后调用了net/http.Response.Body.Close()。
在net/http.Response.Body被关闭后,是否有可能再次读取它?如果不能,那么我想我可以在返回的结构体图中单独存储原始的响应字节。然而,这似乎是多余的,因为字节已经被反序列化了。在这种情况下,我是否应该向库的用户说明响应体已被关闭?
示例程序
package main
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"time"
)
// 用于HTTP请求头的常量
const (
acceptHeaderKey string = "Accept"
acceptHeaderValue string = "application/json"
)
// 结构体,用于模拟从API响应创建的对象图
// 为简洁起见,在此处包含原始JSON字符串,而不是
// 执行从json到结构体的额外反序列化
type Payload struct {
Data string
}
// 结构体,用于封装数据响应
type Data struct {
Payload Payload
Resp *http.Response
}
// 主方法
func main() {
fmt.Println("CREATING CLIENT!")
client := createClient()
fmt.Println("PERFORMING REQUEST!")
data, dErr := doGETRequest(*client)
if dErr != nil {
fmt.Printf("Error encountered performing HTTP request : %v\n", dErr)
} else {
fmt.Println("The data retrieved is", data.Payload.Data)
}
fmt.Println("READING ORIGINAL RESPONSE BODY")
bytes, rdErr := ioutil.ReadAll(data.Resp.Body)
if rdErr != nil {
fmt.Println("Error encountered reading original response body :", rdErr)
} else {
respBytes, minErr := minify(bytes)
if minErr != nil {
fmt.Println("Error encountered while minifying response : ", minErr)
fmt.Printf("Length of bytes read from body using 'ioutil.ReadAll' is %d\n\n", len(bytes))
} else {
fmt.Println("Minified response is : ", string(respBytes))
}
}
}
// 创建一个http客户端实例
func createClient() *http.Client {
client := &http.Client{
Transport: &http.Transport{
MaxIdleConnsPerHost: 30,
},
Timeout: time.Duration(10) * time.Second,
}
return client
}
// 向jsontest.com发起请求
func doGETRequest(client http.Client) (*Data, error) {
// 创建请求的URL
u, err := url.Parse("https://jsonplaceholder.typicode.com/posts")
if err != nil {
fmt.Println("ERROR CREATING URL")
return nil, err
}
// 准备HTTP请求并添加头信息
req, rErr := http.NewRequest("GET", u.String(), nil)
if err != nil {
return nil, rErr
}
addRequestHeaders(req)
// 执行HTTP请求
resp, dErr := client.Do(req)
if dErr != nil {
fmt.Println("ERROR PERFORMING REQUEST")
return nil, dErr
}
// 确保在退出函数时关闭响应体
defer resp.Body.Close()
bytes, rdErr := ioutil.ReadAll(resp.Body)
if rdErr != nil {
fmt.Println("ERROR READING RESPONSE BODY")
return nil, rdErr
}
return &Data{
Payload: Payload{Data: string(bytes)},
Resp: resp,
}, nil
}
// 为接受json添加请求头
func addRequestHeaders(req *http.Request) {
req.Header.Add(acceptHeaderKey, acceptHeaderValue)
}
// 压缩json字节
func minify(jsonB []byte) ([]byte, error) {
var buff *bytes.Buffer = new(bytes.Buffer)
errCompact := json.Compact(buff, jsonB)
if errCompact != nil {
newErr := fmt.Errorf("failure encountered compacting json := %v", errCompact)
return []byte{}, newErr
}
b, err := ioutil.ReadAll(buff)
if err != nil {
readErr := fmt.Errorf("read buffer error encountered := %v", err)
return []byte{}, readErr
}
return b, nil
}
更多关于Golang中如何读取已关闭的net/http.Response:自定义API客户端实现的实战教程也可以访问 https://www.itying.com/category-94-b0.html
好的,明白了,谢谢。非常感谢 🙂
更多关于Golang中如何读取已关闭的net/http.Response:自定义API客户端实现的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
你不能读取一个响应体两次。响应体是流经网络的数据包流。当你调用 Read 时,数据被复制到传入 Read 的 []byte 缓冲区中,然后底层的网络栈会释放其持有的数据副本。如果你想重新读取数据,你必须自己将其存储在某个临时位置,例如通过将其复制到 *bytes.Buffer 或 *os.File 中。
在Go语言中,net/http.Response.Body是一个io.ReadCloser接口,一旦调用Close()方法后,再次读取会返回io.EOF错误。你的代码中defer resp.Body.Close()已经关闭了响应体,所以后续读取会失败。
以下是解决方案的示例代码:
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"time"
)
const (
acceptHeaderKey string = "Accept"
acceptHeaderValue string = "application/json"
)
type Payload struct {
Data string
RawBody []byte // 存储原始响应体
}
type Data struct {
Payload Payload
Resp *http.Response
}
func main() {
client := createClient()
data, dErr := doGETRequest(*client)
if dErr != nil {
fmt.Printf("Error: %v\n", dErr)
return
}
fmt.Println("READING STORED RESPONSE BODY")
respBytes, minErr := minify(data.Payload.RawBody)
if minErr != nil {
fmt.Println("Error minifying:", minErr)
} else {
fmt.Println("Minified response:", string(respBytes))
}
}
func createClient() *http.Client {
return &http.Client{
Transport: &http.Transport{
MaxIdleConnsPerHost: 30,
},
Timeout: 10 * time.Second,
}
}
func doGETRequest(client http.Client) (*Data, error) {
u, err := url.Parse("https://jsonplaceholder.typicode.com/posts")
if err != nil {
return nil, err
}
req, _ := http.NewRequest("GET", u.String(), nil)
addRequestHeaders(req)
resp, dErr := client.Do(req)
if dErr != nil {
return nil, dErr
}
defer resp.Body.Close()
// 读取并存储原始响应体
rawBody, rdErr := io.ReadAll(resp.Body)
if rdErr != nil {
return nil, rdErr
}
return &Data{
Payload: Payload{
Data: string(rawBody),
RawBody: rawBody, // 存储原始字节
},
Resp: resp,
}, nil
}
func addRequestHeaders(req *http.Request) {
req.Header.Add(acceptHeaderKey, acceptHeaderValue)
}
func minify(jsonB []byte) ([]byte, error) {
var buff bytes.Buffer
errCompact := json.Compact(&buff, jsonB)
if errCompact != nil {
return nil, fmt.Errorf("failure compacting json: %v", errCompact)
}
return buff.Bytes(), nil
}
或者使用io.TeeReader在读取时同时保存副本:
func doGETRequest(client http.Client) (*Data, error) {
u, _ := url.Parse("https://jsonplaceholder.typicode.com/posts")
req, _ := http.NewRequest("GET", u.String(), nil)
addRequestHeaders(req)
resp, _ := client.Do(req)
defer resp.Body.Close()
var buf bytes.Buffer
teeReader := io.TeeReader(resp.Body, &buf)
// 第一次读取用于数据处理
processedBody, _ := io.ReadAll(teeReader)
// buf中保存了完整的响应体副本
return &Data{
Payload: Payload{
Data: string(processedBody),
RawBody: buf.Bytes(),
},
Resp: resp,
}, nil
}
对于单元测试,可以直接使用存储的RawBody进行比较:
func TestResponse(t *testing.T) {
data, err := doGETRequest(client)
if err != nil {
t.Fatal(err)
}
// 使用存储的原始响应体
expected := `{"test":"data"}`
minified, _ := minify(data.Payload.RawBody)
if string(minified) != expected {
t.Errorf("Got %s, want %s", string(minified), expected)
}
}
响应体关闭后无法再次读取,必须在关闭前保存副本。存储原始字节是最直接的解决方案。

