Golang中如何重复读取HTTP客户端响应
Golang中如何重复读取HTTP客户端响应 你好,
这里我需要读取客户端响应两次,以便能够:
- 从
Get处理程序返回它。 - 使用
Log函数将其转储到文件中。
请问最节省内存的方法是什么?我查看了 TeeReader 但有些困惑。另外,也不确定在何处以及如何关闭响应体。
谢谢
package api
func Get(w http.ResponseWriter, r *http.Request) {
req, err := http.NewRequestWithContext(r.Context(), http.MethodGet, "https://api.fake.io/id-1", nil)
if err != nil {
w.WriteHeader(500)
return
}
res, err := http.DefaultTransport.RoundTrip(req)
if err != nil {
w.WriteHeader(500)
return
}
defer audit.Log(req, res)
dat, err := io.ReadAll(res.Body)
if err != nil {
w.WriteHeader(500)
return
}
defer res.Body.Close()
w.Write(dat)
}
package audit
func Log(req *http.Request, res *http.Response) error {
dump, err := httputil.DumpResponse(res, true)
if err != nil {
return err
}
buf := &bytes.Buffer{}
buf.WriteString(fmt.Sprintf("%s %s HTTP/%d.%d\n", req.Method, req.URL.RequestURI(), req.ProtoMajor, req.ProtoMinor))
buf.Write(dump)
file, err := os.OpenFile(uuid.NewString() + ".log", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.FileMode(0600))
if err != nil {
return err
}
defer file.Close()
if _, err := file.Write(buf.Bytes()); err != nil {
return err
}
return nil
}
更多关于Golang中如何重复读取HTTP客户端响应的实战教程也可以访问 https://www.itying.com/category-94-b0.html
不,因为你使用了 io.Copy,第一次 buf 复制数据时,它就已经返回了 http.OK,所以再返回 500 状态码是没有意义的。
正如我在回答中所说,它已经解决了你的两个问题。如果你不明白我为什么这样写,那么你应该阅读其中的逻辑。 最大限度地节省内存意味着尽可能广泛地共享底层数据。
根据你的代码,只需在这里移除 defer:
defer audit.Log(req, res)
DumpResponse 在内部会复制响应体。
我考虑了大家所说的,最终得到了这个方案,如果你愿意审阅一下以防我理解有误的话。
确实如此。我复制/粘贴了那段代码,但你说得对,在 io.Copy 之后返回 500 是不正确的。我想,除非它在第一次写入之前就失败了,因此没有设置 http.OK。
func main() {
fmt.Println("hello world")
}
抱歉,我还是不明白,当我说不能移除 defer 时,这个回答是如何解决问题的。
最大程度的内存节省意味着尽可能共享底层数据。
让我们重新开始。请问,在不移除 defer 的情况下,如何实现这个目标?如果无法实现,请给我展示另一个仍然保留 defer 的示例。
audit.Log 的设计目的就是要在后台运行而不阻塞流程,因此它被 defer 了。我本可以自己移除它,但这并不适用。
此外,我问的是“请问最节省内存的方式是什么?”,而不是“我如何在不创建响应副本的情况下实现这一点?”。
由于这两个特定原因,我认为这些回答是多余的。
如果你将整个请求解析到一个结构体中,无论如何它都会被完全读入内存。
说得好。我在原问题中忽略了这一点。
最好直接使用你已经创建的字节数组,并传递一个指向它的指针给你的审计方法。
是的——不过由于它是切片,如果你不传递指针,你传递的只是切片头,而不是底层数组。所以我认为在分配/内存占用方面,这两者之间不会有太大区别:
// 传递一个指向我们切片头的指针。
func DoSomething(b *[]byte)
// 传递切片头;但切片头包含指向底层数组的指针,所以数组不会被复制。
func DoSomething(b []byte)
无论如何,我完全同意你的观点。修改代码,只读取请求体一次。然后使用一个 goroutine 来处理耗时的部分(上传到 S3)。提取 DumpRequest 的相关部分,避免创建响应体的多个副本。就这样搞定。
1- 读取它,将其解析为结构体,并在处理程序中交给我的客户端。
如果你将整个请求解析到一个结构体中,无论如何它都会被完全读入内存。正如 @Dean_Davidson 所说——在你的情况下,流式处理不会带来任何可衡量的好处。最好直接使用你已经创建的字节数组,并将其指针传递给你的审计方法。
最好的方法可能是同步读取请求的URL、方法等信息(如果你想的话,可以使用 defer),但实际的写入S3操作在一个单独的 goroutine 中执行。这个 goroutine 不会有数据竞争,因为它只读取常量数据(字节数组以及你从请求中创建的字符串)。
将审计数据的实际写入操作放到一个单独的 goroutine 中是有益的,因为当原始的 HTTP 处理程序返回时,HTTP 连接就会被释放。如果只使用 defer(而不使用额外的 goroutine),它仍然会阻塞,直到延迟的方法执行完毕才会从调用中返回。这样,你的 HTTP 请求和底层连接将会一直被阻塞,直到审计日志写入完成。
以下是我在后台实际执行的操作:
- 向远程服务器发送一个 HTTP 请求。
- 对来自远程服务器的 HTTP 响应做两件事:
- 读取它,将其解析为结构体,并在处理程序中交给我的客户端。
- 将整个响应(URI、头部和正文……)转储到一个地方(因为我正在审计所有响应): a) 如果应用程序在本地环境中运行,则转储到 TXT 文件。 b) 如果应用程序不在本地环境中运行,则转储到 S3 存储桶。
我只是不希望 a 和 b 这两项操作延迟我向客户端的响应,因为它们有时会花费很长时间。这就是我使用 defer 的原因。
所以我想你的意思是:
- 不要使用
defer,而是像go audit.Log(...)这样使用,并管理当前发生的DATA RACE。 - 使用流式处理,而不是
io.ReadAll(res.Body)。我猜你的意思是var buf bytes.Buffer-io.Copy(&buf, resp.Body)?
顺便说一句,如果你能想到任何更好的解决方案,我很乐意听听。谢谢。
audit.Log的设计目的就是要在后台运行而不阻塞流程,因此它被defer了。我本也可以自己移除它,但这并不适用。
好吧,使用 defer 并不是实现调试任务不阻塞 Web 请求的唯一方法。你可以直接启动一个 goroutine,在那里执行 audit.Log 相关操作。我经常出于这个原因,将 Web 处理程序产生的副作用操作放在它们自己的 goroutine 中执行。
另外,我问的是“请问最节省内存的方式是什么?”,而不是“我如何在不创建响应副本的情况下实现这个?”。
你原来的代码使用了 io.ReadAll(res.Body),这会将整个响应读入内存。如果你希望处理大型请求,就需要将 res.Body 流式传输到缓冲区并进行处理。但一般来说,在 Web 服务器中,我们只是使用 http.MaxBytesReader 将请求大小限制在合理的默认值,除非是在我们预期会收到大响应的处理程序中,否则我们不必担心这个问题。而在那种情况下,是的,你必须进行流式处理。
你是否预期会收到大响应,因此需要进行流式处理?如果是,请查看这个答案:
你可以实现类似的功能,但要与 io.MultiWriter 结合使用。
综上所述,如果你不预期会收到大响应,那么现在这样做可能得不偿失。你分析过你的应用吗?它是否使用了太多内存?你确定这是有问题的代码区域吗?
在查阅了这里所有的示例和建议后,我不确定如何将其应用到下面的代码中。请求体的大小可以是任意的。
package xhttp
import (
"context"
"io"
"net/http"
)
type Client struct {
Client http.RoundTripper
}
func (c Client) Request(ctx context.Context, met, url string, bdy io.Reader, hdrs map[string]string) (*http.Response, error) {
req, err := http.NewRequestWithContext(ctx, met, url, bdy)
if err != nil {
return nil, err
}
for k, v := range hdrs {
req.Header.Add(k, v)
}
// go LogRequest()
res, err := c.Client.RoundTrip(req)
if err != nil {
return nil, err
}
// go LogResponse()
return res, nil
}
// LogRequest writes whole HTTP request to a file.
func LogRequest() {
// GET /api/v1/postcodes?active=false HTTP/1.1
// Host: 0.0.0.0:8080
// Accept: */*
// Accept-Encoding: gzip, deflate, br
// Cache-Control: no-cache
// Connection: keep-alive
// Content-Length: 29
// Content-Type: application/json
// User-Agent: PostmanRuntime/7.42.0
// {
// "postcode": "SW1A0AA"
// }
}
// LogResponse writes whole HTTP response to a file.
func LogResponse() {
// GET /api/v1/postcodes?active=false HTTP/1.1
// X-Request-Id: 1f532ad9-4062-46d3-9e2f-9fd5f3c74d9a
// Content-Type: application/json
//
// Status: 200 OK
//
// {
// "exists": true
// }
}
我不这么认为。我认为 @GonzaSaya 建议你在那里避免使用 defer 的原因是:它会创建响应体的副本,这样你就不会遇到第二次读取时响应体为空的问题。相关代码在这里:
https://cs.opensource.google/go/go/+/refs/tags/go1.23.3:src/net/http/httputil/dump.go;l=281
因此,你可以不延迟执行它,并且为了简化将 resp.Body 复制到 w 的操作,你可以使用 io.Copy 而不是 io.ReadAll,就像这样:
_, err = io.Copy(w, resp.Body)
所以你的伪代码会变成类似这样(可能无法编译;我在这里的文本编辑器中写的,请根据需要调整):
func Get(w http.ResponseWriter, r *http.Request) {
req, err := http.NewRequestWithContext(r.Context(), http.MethodGet, "https://api.fake.io/id-1", nil)
if err != nil {
w.WriteHeader(500) // 你可能应该对 err 做些什么
return
}
res, err := http.DefaultTransport.RoundTrip(req)
if err != nil {
w.WriteHeader(500) // 这里也应该对 err 做些什么
return
}
// 记录请求;从 res 读取但会创建 res.Body 的副本
// 以便我们稍后可以再次读取它。
audit.Log(req, res)
// 将我们复制的 resp.Body 复制到 w
_, err = io.Copy(w, resp.Body)
if err != nil {
w.WriteHeader(500) // 这里也应该对 err 做些什么
}
// 在成功路径上,你可能还应该在这里向 w 写入头部信息等。
res.Body.Close()
}
如果让我来实现这个功能,我会这样做:
func Get(w http.ResponseWriter, r *http.Request) {
req, err := http.NewRequestWithContext(r.Context(), http.MethodGet, "https://api.fake.io/id-1", nil)
if err != nil {
w.WriteHeader(500)
return
}
res, err := http.DefaultTransport.RoundTrip(req)
if err != nil {
w.WriteHeader(500)
return
}
defer res.Body.Close()
bs := new(bytes.Buffer)
writer := io.MultiWriter(w, bs)
_, _ = io.Copy(writer, res.Body) // will 200 Code
// 方案a // 推荐此方案
flush, ok := writer.(http.Flusher)
if ok {
flush.Flush()
}
_ = Log(req, res, bs)
// 方案b
go func() { // 或使用goroutine池;如果只是简单的并发,当短时间内有大量请求时,可能会产生大量的内存开销。
_ = Log(req, res, bs)
}()
}
func Log(req *http.Request, res *http.Response, bs *bytes.Buffer) error {
dump, err := httputil.DumpResponse(res, false)
if err != nil {
return err
}
file, err := os.OpenFile(uuid.NewString()+".log", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.FileMode(0600))
if err != nil {
return err
}
defer file.Close()
_, err = file.WriteString(fmt.Sprintf("%s %s HTTP/%d.%d\n", req.Method, req.URL.RequestURI(), req.ProtoMajor, req.ProtoMinor))
if err != nil {
return err
}
_, err = file.Write(dump)
if err != nil {
return err
}
_, err = file.WriteString("\r\n\r\n")
if err != nil {
return err
}
_, err = io.Copy(file, bs)
if err != nil {
return err
}
return nil
}
在Golang中重复读取HTTP响应体,最节省内存的方法是使用io.TeeReader配合缓冲区。以下是修改后的代码:
package api
import (
"bytes"
"io"
"net/http"
)
func Get(w http.ResponseWriter, r *http.Request) {
req, err := http.NewRequestWithContext(r.Context(), http.MethodGet, "https://api.fake.io/id-1", nil)
if err != nil {
w.WriteHeader(500)
return
}
res, err := http.DefaultTransport.RoundTrip(req)
if err != nil {
w.WriteHeader(500)
return
}
defer res.Body.Close()
// 创建缓冲区来存储响应体副本
var buf bytes.Buffer
teeReader := io.TeeReader(res.Body, &buf)
// 先读取到缓冲区(用于日志)
bodyForLog, err := io.ReadAll(teeReader)
if err != nil {
w.WriteHeader(500)
return
}
// 创建新的响应体用于日志函数
resForLog := *res
resForLog.Body = io.NopCloser(bytes.NewReader(bodyForLog))
// 调用日志函数
if err := audit.Log(req, &resForLog); err != nil {
w.WriteHeader(500)
return
}
// 将缓冲区内容写回响应
w.Write(buf.Bytes())
}
package audit
import (
"bytes"
"fmt"
"net/http"
"net/http/httputil"
"os"
"github.com/google/uuid"
)
func Log(req *http.Request, res *http.Response) error {
dump, err := httputil.DumpResponse(res, true)
if err != nil {
return err
}
buf := &bytes.Buffer{}
buf.WriteString(fmt.Sprintf("%s %s HTTP/%d.%d\n",
req.Method, req.URL.RequestURI(), req.ProtoMajor, req.ProtoMinor))
buf.Write(dump)
file, err := os.OpenFile(uuid.NewString()+".log",
os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.FileMode(0600))
if err != nil {
return err
}
defer file.Close()
if _, err := file.Write(buf.Bytes()); err != nil {
return err
}
return nil
}
或者使用更高效的方法,直接复用缓冲区:
package api
import (
"bytes"
"io"
"net/http"
)
func Get(w http.ResponseWriter, r *http.Request) {
req, err := http.NewRequestWithContext(r.Context(), http.MethodGet, "https://api.fake.io/id-1", nil)
if err != nil {
w.WriteHeader(500)
return
}
res, err := http.DefaultTransport.RoundTrip(req)
if err != nil {
w.WriteHeader(500)
return
}
defer res.Body.Close()
// 一次性读取响应体
bodyBytes, err := io.ReadAll(res.Body)
if err != nil {
w.WriteHeader(500)
return
}
// 创建响应副本用于日志
resForLog := *res
resForLog.Body = io.NopCloser(bytes.NewReader(bodyBytes))
// 异步记录日志
go func() {
_ = audit.Log(req, &resForLog)
}()
// 返回响应
w.Write(bodyBytes)
}
关于响应体关闭:只需要在Get函数中调用一次defer res.Body.Close()即可。当使用io.NopCloser包装的reader不需要单独关闭,因为它只是一个包装器,不持有实际资源。


