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

3 回复

好的,明白了,谢谢。非常感谢 🙂

更多关于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)
    }
}

响应体关闭后无法再次读取,必须在关闭前保存副本。存储原始字节是最直接的解决方案。

回到顶部