Golang性能提升利器之SectionReader的用法詳解
一. 簡(jiǎn)介
本文將介紹 Go 語(yǔ)言中的 SectionReader,包括 SectionReader的基本使用方法、實(shí)現(xiàn)原理、使用注意事項(xiàng)。從而能夠在合適的場(chǎng)景下,更好得使用SectionReader類型,提升程序的性能。
二. 問題引入
這里我們需要實(shí)現(xiàn)一個(gè)基本的HTTP文件服務(wù)器功能,可以處理客戶端的HTTP請(qǐng)求來讀取指定文件,并根據(jù)請(qǐng)求的Range頭部字段返回文件的部分?jǐn)?shù)據(jù)或整個(gè)文件數(shù)據(jù)。
這里一個(gè)簡(jiǎn)單的思路,可以先把整個(gè)文件的數(shù)據(jù)加載到內(nèi)存中,然后再根據(jù)請(qǐng)求指定的范圍,截取對(duì)應(yīng)的數(shù)據(jù)返回回去即可。下面提供一個(gè)代碼示例:
func serveFile(w http.ResponseWriter, r *http.Request, filePath string) {
// 打開文件
file, _ := os.Open(filePath)
defer file.Close()
// 讀取整個(gè)文件數(shù)據(jù)
fileData, err := ioutil.ReadAll(file)
if err != nil {
// 錯(cuò)誤處理
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// 根據(jù)Range頭部字段解析請(qǐng)求的范圍
rangeHeader := r.Header.Get("Range")
ranges, err := parseRangeHeader(rangeHeader)
if err != nil {
// 錯(cuò)誤處理
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// 處理每個(gè)范圍并返回?cái)?shù)據(jù)
for _, rng := range ranges {
start := rng.Start
end := rng.End
// 從文件數(shù)據(jù)中提取范圍的字節(jié)數(shù)據(jù)
rangeData := fileData[start : end+1]
// 將范圍數(shù)據(jù)寫入響應(yīng)
w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, fileInfo.Size()))
w.Header().Set("Content-Length", strconv.Itoa(len(rangeData)))
w.WriteHeader(http.StatusPartialContent)
w.Write(rangeData)
}
}
type Range struct {
Start int
End int
}
// 解析HTTP Range請(qǐng)求頭
func parseRangeHeader(rangeHeader string) ([]Range, error){}上述的代碼實(shí)現(xiàn)比較簡(jiǎn)單,首先,函數(shù)打開filePath指定的文件,使用ioutil.ReadAll函數(shù)讀取整個(gè)文件的數(shù)據(jù)到fileData中。接下來,從HTTP請(qǐng)求頭中Range頭部字段中獲取范圍信息,獲取每個(gè)范圍請(qǐng)求的起始和終止位置。接著,函數(shù)遍歷每一個(gè)范圍信息,提取文件數(shù)據(jù)fileData 中對(duì)應(yīng)范圍的字節(jié)數(shù)據(jù)到rangeData中,然后將數(shù)據(jù)返回回去?;诖?,簡(jiǎn)單實(shí)現(xiàn)了一個(gè)支持范圍請(qǐng)求的HTTP文件服務(wù)器。
但是當(dāng)前實(shí)現(xiàn)其實(shí)存在一個(gè)問題,即在每次請(qǐng)求都會(huì)將整個(gè)文件加載到內(nèi)存中,即使用戶只需要讀取其中一小部分?jǐn)?shù)據(jù),這種處理方式會(huì)給內(nèi)存帶來非常大的壓力。假如被請(qǐng)求文件的大小是100M,一個(gè)32G內(nèi)存的機(jī)器,此時(shí)最多只能支持320個(gè)并發(fā)請(qǐng)求。但是用戶每次請(qǐng)求可能只是讀取文件的一小部分?jǐn)?shù)據(jù),比如1M,此時(shí)將整個(gè)文件加載到內(nèi)存中,往往是一種資源的浪費(fèi),同時(shí)從磁盤中讀取全部數(shù)據(jù)到內(nèi)存中,此時(shí)性能也較低。
那能不能在處理請(qǐng)求時(shí),HTTP文件服務(wù)器只讀取請(qǐng)求的那部分?jǐn)?shù)據(jù),而不是加載整個(gè)文件的內(nèi)容,go基礎(chǔ)庫(kù)有對(duì)應(yīng)類型的支持嗎?
其實(shí)還真有,Go語(yǔ)言中其實(shí)存在一個(gè)SectionReader的類型,它可以從一個(gè)給定的數(shù)據(jù)源中讀取數(shù)據(jù)的特定片段,而不是讀取整個(gè)數(shù)據(jù)源,這個(gè)類型在這個(gè)場(chǎng)景下使用非常合適。
下面我們先仔細(xì)介紹下SectionReader的基本使用方式,然后將其作用到上面文件服務(wù)器的實(shí)現(xiàn)當(dāng)中。
三. 基本使用
3.1 基本定義
SectionReader類型的定義如下:
type SectionReader struct {
r ReaderAt
base int64
off int64
limit int64
}SectionReader包含了四個(gè)字段:
r:一個(gè)實(shí)現(xiàn)了ReaderAt接口的對(duì)象,它是數(shù)據(jù)源。base: 數(shù)據(jù)源的起始位置,通過設(shè)置base字段,可以調(diào)整數(shù)據(jù)源的起始位置。off:讀取的起始位置,表示從數(shù)據(jù)源的哪個(gè)偏移量開始讀取數(shù)據(jù),初始化時(shí)一般與base保持一致。limit:數(shù)據(jù)讀取的結(jié)束位置,表示讀取到哪里結(jié)束。
同時(shí)還提供了一個(gè)構(gòu)造器方法,用于創(chuàng)建一個(gè)SectionReader實(shí)例,定義如下:
func NewSectionReader(r ReaderAt, off int64, n int64) *SectionReader {
// ... 忽略一些驗(yàn)證邏輯
// remaining 代表數(shù)據(jù)讀取的結(jié)束位置,為 base(偏移量) + n(讀取字節(jié)數(shù))
remaining = n + off
return &SectionReader{r, off, off, remaining}
}NewSectionReader接收三個(gè)參數(shù),r 代表實(shí)現(xiàn)了ReadAt接口的數(shù)據(jù)源,off表示起始位置的偏移量,也就是要從哪里開始讀取數(shù)據(jù),n代表要讀取的字節(jié)數(shù)。通過NewSectionReader函數(shù),可以很方便得創(chuàng)建出SectionReader對(duì)象,然后讀取特定范圍的數(shù)據(jù)。
3.2 使用方式
SectionReader 能夠像io.Reader一樣讀取數(shù)據(jù),唯一區(qū)別是會(huì)被限定在指定范圍內(nèi),只會(huì)返回特定范圍的數(shù)據(jù)。
下面通過一個(gè)例子來說明SectionReader的使用,代碼示例如下:
package main
import (
"fmt"
"io"
"strings"
)
func main() {
// 一個(gè)實(shí)現(xiàn)了 ReadAt 接口的數(shù)據(jù)源
data := strings.NewReader("Hello,World!")
// 創(chuàng)建 SectionReader,讀取范圍為索引 2 到 9 的字節(jié)
// off = 2, 代表從第二個(gè)字節(jié)開始讀取; n = 7, 代表讀取7個(gè)字節(jié)
section := io.NewSectionReader(data, 2, 7)
// 數(shù)據(jù)讀取緩沖區(qū)長(zhǎng)度為5
buffer := make([]byte, 5)
for {
// 不斷讀取數(shù)據(jù),直到返回io.EOF
n, err := section.Read(buffer)
if err != nil {
if err == io.EOF {
// 已經(jīng)讀取到末尾,退出循環(huán)
break
}
fmt.Println("Error:", err)
return
}
fmt.Printf("Read %d bytes: %s\n", n, buffer[:n])
}
}上述函數(shù)使用 io.NewSectionReader 創(chuàng)建了一個(gè) SectionReader,指定了開始讀取偏移量為 2,讀取字節(jié)數(shù)為 7。這意味著我們將從第三個(gè)字節(jié)(索引 2)開始讀取,讀取 7 個(gè)字節(jié)。
然后我們通過一個(gè)無限循環(huán),不斷調(diào)用Read方法讀取數(shù)據(jù),直到讀取完所有的數(shù)據(jù)。函數(shù)運(yùn)行結(jié)果如下,確實(shí)只讀取了范圍為索引 2 到 9 的字節(jié)的內(nèi)容:
Read 5 bytes: llo,W
Read 2 bytes: or
因此,如果我們只需要讀取數(shù)據(jù)源的某一部分?jǐn)?shù)據(jù),此時(shí)可以創(chuàng)建一個(gè)SectionReader實(shí)例,定義好數(shù)據(jù)讀取的偏移量和數(shù)據(jù)量之后,之后可以像普通的io.Reader那樣讀取數(shù)據(jù),SectionReader確保只會(huì)讀取到指定范圍的數(shù)據(jù)。
3.3 使用例子
這里回到上面HTTP文件服務(wù)器實(shí)現(xiàn)的例子,之前的實(shí)現(xiàn)存在一個(gè)問題,即每次請(qǐng)求都會(huì)讀取整個(gè)文件的內(nèi)容,這會(huì)代碼內(nèi)存資源的浪費(fèi),性能低,響應(yīng)時(shí)間比較長(zhǎng)等問題。下面我們使用SectionReader 對(duì)其進(jìn)行優(yōu)化,實(shí)現(xiàn)如下:
func serveFile(w http.ResponseWriter, r *http.Request, filePath string) {
// 打開文件
file, err := os.Open(filePath)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer file.Close()
// 獲取文件信息
fileInfo, err := file.Stat()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// 根據(jù)Range頭部字段解析請(qǐng)求的范圍
rangeHeader := r.Header.Get("Range")
ranges, err := parseRangeHeader(rangeHeader)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// 處理每個(gè)范圍并返回?cái)?shù)據(jù)
for _, rng := range ranges {
start := rng.Start
end := rng.End
// 根據(jù)范圍創(chuàng)建SectionReader
section := io.NewSectionReader(file, int64(start), int64(end-start+1))
// 將范圍數(shù)據(jù)寫入響應(yīng)
w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, fileInfo.Size()))
w.WriteHeader(http.StatusPartialContent)
io.CopyN(w, section, section.Size())
}
}
type Range struct {
Start int
End int
}
// 解析HTTP Range請(qǐng)求頭
func parseRangeHeader(rangeHeader string) ([]Range, error) {}在上述優(yōu)化后的實(shí)現(xiàn)中,我們使用 io.NewSectionReader 創(chuàng)建了 SectionReader,它的范圍是根據(jù)請(qǐng)求頭中的范圍信息計(jì)算得出的。然后,我們通過 io.CopyN 將 SectionReader 中的數(shù)據(jù)直接拷貝到響應(yīng)的 http.ResponseWriter 中。
上述兩個(gè)HTTP文件服務(wù)器實(shí)現(xiàn)的區(qū)別,只在于讀取特定范圍數(shù)據(jù)方式,前一種方式是將整個(gè)文件加載到內(nèi)存中,再截取特定范圍的數(shù)據(jù);而后者則是通過使用 SectionReader,我們避免了一次性讀取整個(gè)文件數(shù)據(jù),并且只讀取請(qǐng)求范圍內(nèi)的數(shù)據(jù)。這種優(yōu)化能夠更高效地處理大文件或處理大量并發(fā)請(qǐng)求的場(chǎng)景,節(jié)省了內(nèi)存和處理時(shí)間。
四. 實(shí)現(xiàn)原理
4.1 設(shè)計(jì)初衷
SectionReader的設(shè)計(jì)初衷,在于提供一種簡(jiǎn)潔,靈活的方式來讀取數(shù)據(jù)源的特定部分。
4.2 基本原理
SectionReader 結(jié)構(gòu)體中off,base,limit字段是實(shí)現(xiàn)只讀取數(shù)據(jù)源特定部分?jǐn)?shù)據(jù)功能的重要變量。
type SectionReader struct {
r ReaderAt
base int64
off int64
limit int64
}由于SectionReader需要保證只讀取特定范圍的數(shù)據(jù),故需要保存開始位置和結(jié)束位置的值。這里是通過base和limit這兩個(gè)字段來實(shí)現(xiàn)的,base記錄了數(shù)據(jù)讀取的開始位置,limit記錄了數(shù)據(jù)讀取的結(jié)束位置。
通過設(shè)定base和limit兩個(gè)字段的值,限制了能夠被讀取數(shù)據(jù)的范圍。之后需要開始讀取數(shù)據(jù),有可能這部分待讀取的數(shù)據(jù)不會(huì)被一次性讀完,此時(shí)便需要一個(gè)字段來說明接下來要從哪一個(gè)字節(jié)繼續(xù)讀取下去,因此SectionReader也設(shè)置了off字段的值,這個(gè)代表著下一個(gè)帶讀取數(shù)據(jù)的位置。
在使用SectionReader讀取數(shù)據(jù)的過程中,通過base和limit限制了讀取數(shù)據(jù)的范圍,off則不斷修改,指向下一個(gè)帶讀取的字節(jié)。
4.3 代碼實(shí)現(xiàn)
4.3.1 Read方法說明
func (s *SectionReader) Read(p []byte) (n int, err error) {
// s.off: 將被讀取數(shù)據(jù)的下標(biāo)
// s.limit: 指定讀取范圍的最后一個(gè)字節(jié),這里應(yīng)該保證s.base <= s.off
if s.off >= s.limit {
return 0, EOF
}
// s.limit - s.off: 還剩下多少數(shù)據(jù)未被讀取
if max := s.limit - s.off; int64(len(p)) > max {
p = p[0:max]
}
// 調(diào)用 ReadAt 方法讀取數(shù)據(jù)
n, err = s.r.ReadAt(p, s.off)
// 指向下一個(gè)待被讀取的字節(jié)
s.off += int64(n)
return
}SectionReader實(shí)現(xiàn)了Read 方法,通過該方法能夠?qū)崿F(xiàn)指定范圍數(shù)據(jù)的讀取,在內(nèi)部實(shí)現(xiàn)中,通過兩個(gè)限制來保證只會(huì)讀取到指定范圍的數(shù)據(jù),具體限制如下:
- 通過保證
off不大于limit字段的值,保證不會(huì)讀取超過指定范圍的數(shù)據(jù) - 在調(diào)用
ReadAt方法時(shí),保證傳入切片長(zhǎng)度不大于剩余可讀數(shù)據(jù)長(zhǎng)度
通過這兩個(gè)限制,保證了用戶只要設(shè)定好了數(shù)據(jù)開始讀取偏移量 base 和 數(shù)據(jù)讀取結(jié)束偏移量 limit字段值,Read方法便只會(huì)讀取這個(gè)范圍的數(shù)據(jù)。
4.3.2 ReadAt 方法說明
func (s *SectionReader) ReadAt(p []byte, off int64) (n int, err error) {
// off: 參數(shù)指定了偏移字節(jié)數(shù),為一個(gè)相對(duì)數(shù)值
// s.limit - s.base >= off: 保證不會(huì)越界
if off < 0 || off >= s.limit-s.base {
return 0, EOF
}
// off + base: 獲取絕對(duì)的偏移量
off += s.base
// 確保傳入字節(jié)數(shù)組長(zhǎng)度 不超過 剩余讀取數(shù)據(jù)范圍
if max := s.limit - off; int64(len(p)) > max {
p = p[0:max]
// 調(diào)用ReadAt 方法讀取數(shù)據(jù)
n, err = s.r.ReadAt(p, off)
if err == nil {
err = EOF
}
return n, err
}
return s.r.ReadAt(p, off)
}SectionReader還提供了ReadAt方法,能夠指定偏移量處實(shí)現(xiàn)數(shù)據(jù)讀取。它根據(jù)傳入的偏移量off字段的值,計(jì)算出實(shí)際的偏移量,并調(diào)用底層源的ReadAt方法進(jìn)行讀取操作,在這個(gè)過程中,也保證了讀取數(shù)據(jù)范圍不會(huì)超過base和limit字段指定的數(shù)據(jù)范圍。
這個(gè)方法提供了一種靈活的方式,能夠在限定的數(shù)據(jù)范圍內(nèi),隨意指定偏移量來讀取數(shù)據(jù),不過需要注意的是,該方法并不會(huì)影響實(shí)例中off字段的值。
4.3.3 Seek 方法說明
func (s *SectionReader) Seek(offset int64, whence int) (int64, error) {
switch whence {
default:
return 0, errWhence
case SeekStart:
// s.off = s.base + offset
offset += s.base
case SeekCurrent:
// s.off = s.off + offset
offset += s.off
case SeekEnd:
// s.off = s.limit + offset
offset += s.limit
}
// 檢查
if offset < s.base {
return 0, errOffset
}
s.off = offset
return offset - s.base, nil
}SectionReader也提供了Seek方法,給其提供了隨機(jī)訪問和靈活讀取數(shù)據(jù)的能力。舉個(gè)例子,假如已經(jīng)調(diào)用Read方法讀取了一部分?jǐn)?shù)據(jù),但是想要重新讀取該數(shù)據(jù),此時(shí)便可以使Seek方法將off字段設(shè)置回之前的位置,然后再次調(diào)用Read方法進(jìn)行讀取。
五. 使用注意事項(xiàng)
5.1 注意off值在base和limit之間
當(dāng)使用 SectionReader 創(chuàng)建實(shí)例時(shí),確保 off 值在 base 和 limit 之間是至關(guān)重要的。保證 off 值在 base 和 limit 之間的好處是確保讀取操作在有效的數(shù)據(jù)范圍內(nèi)進(jìn)行,避免讀取錯(cuò)誤或超出范圍的訪問。如果 off 值小于 base 或大于等于 limit,讀取操作可能會(huì)導(dǎo)致錯(cuò)誤或返回 EOF。
一個(gè)良好的實(shí)踐方式是使用 NewSectionReader 函數(shù)來創(chuàng)建 SectionReader 實(shí)例。NewSectionReader 函數(shù)會(huì)檢查 off 值是否在有效范圍內(nèi),并自動(dòng)調(diào)整 off 值,以確保它在 base 和 limit 之間。
5.2 及時(shí)關(guān)閉底層數(shù)據(jù)源
當(dāng)使用SectionReader時(shí),如果沒有及時(shí)關(guān)閉底層數(shù)據(jù)源可能會(huì)導(dǎo)致資源泄露,這些資源在程序執(zhí)行期間將一直保持打開狀態(tài),直到程序終止。在處理大量請(qǐng)求或長(zhǎng)時(shí)間運(yùn)行的情況下,可能會(huì)耗盡系統(tǒng)的資源。
下面是一個(gè)示例,展示了沒有關(guān)閉SectionReader底層數(shù)據(jù)源可能引發(fā)的問題:
func main() {
file, err := os.Open("data.txt")
if err != nil {
log.Fatal(err)
}
defer file.Close()
section := io.NewSectionReader(file, 10, 20)
buffer := make([]byte, 10)
_, err = section.Read(buffer)
if err != nil {
log.Fatal(err)
}
// 沒有關(guān)閉底層數(shù)據(jù)源,可能導(dǎo)致資源泄露或其他問題
}在上述示例中,底層數(shù)據(jù)源是一個(gè)文件。在程序結(jié)束時(shí),沒有顯式調(diào)用file.Close()來關(guān)閉文件句柄,這將導(dǎo)致文件資源一直保持打開狀態(tài),直到程序終止。這可能導(dǎo)致其他進(jìn)程無法訪問該文件或其他與文件相關(guān)的問題。
因此,在使用SectionReader時(shí),要注意及時(shí)關(guān)閉底層數(shù)據(jù)源,以確保資源的正確管理和避免潛在的問題。
六. 總結(jié)
本文主要對(duì)SectionReader進(jìn)行了介紹。文章首先從一個(gè)基本HTTP文件服務(wù)器的功能實(shí)現(xiàn)出發(fā),解釋了該實(shí)現(xiàn)存在內(nèi)存資源浪費(fèi),并發(fā)性能低等問題,從而引出了SectionReader。
接下來介紹了SectionReader的基本定義,以及其基本使用方法,最后使用SectionReader對(duì)上述HTTP文件服務(wù)器進(jìn)行優(yōu)化。接著還詳細(xì)講述了SectionReader的實(shí)現(xiàn)原理,從而能夠更好得理解和使用SectionReader。
最后,講解了SectionReader的使用注意事項(xiàng),如需要及時(shí)關(guān)閉底層數(shù)據(jù)源等。基于此完成了SectionReader的介紹。
以上就是Golang性能提升利器之SectionReader的用法詳解的詳細(xì)內(nèi)容,更多關(guān)于Golang SectionReader的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
golang實(shí)現(xiàn)unicode轉(zhuǎn)換為字符串string的方法
這篇文章主要介紹了golang實(shí)現(xiàn)unicode轉(zhuǎn)換為字符串string的方法,實(shí)例分析了Go語(yǔ)言編碼轉(zhuǎn)換的相關(guān)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2016-07-07
Golang實(shí)現(xiàn)http文件上傳小功能的案例
這篇文章主要介紹了Golang實(shí)現(xiàn)http文件上傳小功能的案例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2021-05-05

