golang多次讀取http request body的問題分析
問題起因
使用postman發(fā)送了一個(gè)http請求,對每個(gè)請求都有一個(gè)對應(yīng)的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結(jié)構(gòu)(這里貼出部分成員變量):
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發(fā)現(xiàn)讀取出來的字節(jié)切片為空:
func ApiHandler() (error) {
...
bodyBytes, err := ioutil.ReadAll(request.Request.Body)
// fmt.Printf("bodyBytes: %+v", bodyBytes) 結(jié)果為[]
if err != nil {
...
}
...
}
問題探究
我把這個(gè)問題發(fā)給了gpt

gpt回答說可能是由于Body已經(jīng)被讀取過一次,事實(shí)上,我的代碼之前確實(shí)使用過ReadBody方法讀取了一次:
func ApiHandler2() (error) {
input, err := parse.ReadBody(request.Request)
...
}
這個(gè)parse.ReadBody是公司的庫代碼,在此不深入分析
出于好奇,我問了gpt官方庫中ioutil.ReadAll()方法能否多次讀取Request.Body

gpt回答ReadAll()方法讀取了一次就會消耗掉Request.Body,不能再次讀取,并提供兩種方法再次讀取:
- 將讀取的Request.Body緩存到一個(gè)變量bodyBytes(字節(jié)切片類型),后續(xù)需要讀取
- 使用該變量使用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
}
}
}
函數(shù)的作用是初始化一個(gè)字節(jié)切片緩沖區(qū),不斷調(diào)用Read方法讀取數(shù)據(jù),直到EOF為止
緩沖區(qū)b的初始大小只有512個(gè)字節(jié),如果緩沖區(qū)滿(len(b)==cap(b)),則向b添加一個(gè)0元素觸發(fā)切片的擴(kuò)容機(jī)制,并去掉添加的"0"元素([:len(b)]),之后一直讀取數(shù)據(jù),可能緩沖區(qū)又會滿,會繼續(xù)擴(kuò)容的操作,直到讀取到EOF
從對ReadAll()方法的分析可以得知,使用ReadAll函數(shù)處理數(shù)據(jù)時(shí),內(nèi)存消耗隨著數(shù)據(jù)的增大而增加,處理較大數(shù)據(jù)時(shí),會觸發(fā)多次擴(kuò)容機(jī)制,需要分配大量內(nèi)存。加載一個(gè)10M的文件,可能就需要50M的內(nèi)存分配
io.Reader 接口中的 Read 方法的定義如下:
type Reader interface {
Read(p []byte) (n int, err error)
}
這個(gè)方法接收一個(gè)字節(jié)數(shù)組 p 作為參數(shù),返回兩個(gè)值,一個(gè)是 n 表示讀取的字節(jié)數(shù),另一個(gè)是 err 表示可能出現(xiàn)的錯(cuò)誤。不同的數(shù)據(jù)源類型實(shí)現(xiàn)方式不同
Request.Body只能讀取一次的原因是因?yàn)椋诘谝淮螌ζ溥M(jìn)行讀取時(shí),指針已經(jīng)移動(dòng)到了 EOF(End Of File)位置,再次讀取時(shí)就無法再次從頭開始讀取了
題外話
關(guān)于ReadAll()方法消耗內(nèi)存的替代方案有兩種:
- 使用io.ReadFile函數(shù)
- 使用io.Copy函數(shù)
ReadFile函數(shù)定義如下:
func ReadFile(filename string) ([]byte, error){}
傳參為待加載文件的路徑,返回為文件的內(nèi)容
- io.Copy會拷貝數(shù)據(jù),少于ReadAll的內(nèi)存消耗
以上就是golang多次讀取http request body的問題分析的詳細(xì)內(nèi)容,更多關(guān)于golang多次讀取http request body的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Go語言編譯程序從后臺運(yùn)行,不出現(xiàn)dos窗口的操作
這篇文章主要介紹了Go語言編譯程序從后臺運(yùn)行,不出現(xiàn)dos窗口的操作,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2021-04-04
Golang實(shí)現(xiàn)簡易的rpc調(diào)用
RPC指(Remote Procedure Call Protocol)遠(yuǎn)程過程調(diào)用協(xié)議。本文將實(shí)現(xiàn)利用Golang進(jìn)行rpc調(diào)用(只實(shí)現(xiàn)一個(gè)rpc框架基本的功能,不對性能做保證),需要的可以參考一下2023-03-03
Windows10系統(tǒng)下安裝Go環(huán)境詳細(xì)步驟
Go語言是谷歌推出的一款全新的編程語言,可以在不損失應(yīng)用程序性能的情況下極大的降低代碼的復(fù)雜性,這篇文章主要給大家介紹了關(guān)于Windows10系統(tǒng)下安裝Go環(huán)境的詳細(xì)步驟,需要的朋友可以參考下2023-11-11
Golang實(shí)現(xiàn)支付寶沙箱支付的方法步驟
本文主要介紹了Golang實(shí)現(xiàn)支付寶沙箱支付的方法步驟,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-04-04
淺析Go語言如何在終端里實(shí)現(xiàn)倒計(jì)時(shí)
這篇文章主要為大家詳細(xì)介紹了Go語言中是如何在終端里實(shí)現(xiàn)倒計(jì)時(shí)的,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2025-03-03

