问题起因
使用postman发送了一个http请求,对每个请求都有一个对应的context:
type APIContext struct {
Action string
ID string
Type string
Link string
Method string
Version *APIVersion
Request *http.Request
Response http.ResponseWriter
...
}
其中Request成员变量是golang1.17.3版本http库中定义的Request结构(这里贴出部分成员变量):
type Request struct {
Method string
URL *url.URL
Header Header
Body io.ReadCloser
GetBody func() (io.ReadCloser, error)
Response *Response
ctx context.Context
...
}
请求处理的代码使用ReadAll方法读取Request.Body,debug发现读取出来的字节切片为空:
func ApiHandler() (error) {
...
bodyBytes, err := ioutil.ReadAll(request.Request.Body)
// fmt.Printf("bodyBytes: %+v", bodyBytes) 结果为[]
if err != nil {
...
}
...
}
问题探究
我把这个问题发给了gpt
gpt回答说可能是由于Body已经被读取过一次,事实上,我的代码之前确实使用过ReadBody方法读取了一次:
func ApiHandler2() (error) {
input, err := parse.ReadBody(request.Request)
...
}
这个parse.ReadBody是公司的库代码,在此不深入分析
出于好奇,我问了gpt官方库中ioutil.ReadAll()方法能否多次读取Request.Body
gpt回答ReadAll()方法读取了一次就会消耗掉Request.Body,不能再次读取,并提供两种方法再次读取:
- 将读取的Request.Body缓存到一个变量bodyBytes(字节切片类型),后续需要读取使用该变量
- 使用ioutol.NopCloser方法写回到Request.Body
问题溯源
来研究一下ioutil.ReadAll()源码:
// ReadAll reads from r until an error or EOF and returns the data it read.
// A successful call returns err == nil, not err == EOF. Because ReadAll is
// defined to read from src until EOF, it does not treat an EOF from Read
// as an error to be reported.
func ReadAll(r Reader) ([]byte, error) {
b := make([]byte, 0, 512)
for {
if len(b) == cap(b) {
// Add more capacity (let append pick how much).
b = append(b, 0)[:len(b)]
}
n, err := r.Read(b[len(b):cap(b)])
b = b[:len(b)+n]
if err != nil {
if err == EOF {
err = nil
}
return b, err
}
}
}
函数的作用是初始化一个字节切片缓冲区,不断调用Read方法读取数据,直到EOF为止
缓冲区b的初始大小只有512个字节,如果缓冲区满(len(b)==cap(b)),则向b添加一个0元素触发切片的扩容机制,并去掉添加的"0"元素([:len(b)]),之后一直读取数据,可能缓冲区又会满,会继续扩容的操作,直到读取到EOF
从对ReadAll()方法的分析可以得知,使用ReadAll函数处理数据时,内存消耗随着数据的增大而增加,处理较大数据时,会触发多次扩容机制,需要分配大量内存。加载一个10M的文件,可能就需要50M的内存分配
io.Reader 接口中的 Read 方法的定义如下:
type Reader interface {
Read(p []byte) (n int, err error)
}
这个方法接收一个字节数组 p 作为参数,返回两个值,一个是 n 表示读取的字节数,另一个是 err 表示可能出现的错误。不同的数据源类型实现方式不同
Request.Body只能读取一次的原因是因为,在第一次对其进行读取时,指针已经移动到了 EOF(End Of File)位置,再次读取时就无法再次从头开始读取了
题外话
关于ReadAll()方法消耗内存的替代方案有两种:文章来源:https://www.toymoban.com/news/detail-837994.html
- 使用io.ReadFile函数
- 使用io.Copy函数
- ReadFile函数定义如下:
func ReadFile(filename string) ([]byte, error){}
传参为待加载文件的路径,返回为文件的内容文章来源地址https://www.toymoban.com/news/detail-837994.html
- io.Copy会拷贝数据,少于ReadAll的内存消耗
到了这里,关于golang多次读取http request body问题分析的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!