在進(jìn)行本地 file
文件內(nèi)容讀取挠说,或進(jìn)行 HTTP
網(wǎng)絡(luò)接口通信的時(shí)候澡谭,我們經(jīng)常使用 io.ReadAll
來(lái)讀取遠(yuǎn)程接口返回的 resp.Body
,但接口返回?cái)?shù)據(jù)量有大有小损俭,io.ReadAll
是怎樣完成全部數(shù)據(jù)的讀取的蛙奖?
帶著此疑問(wèn),讓我們走近 io.ReadAll
源碼一探究竟:
1. Demo 讀取文件內(nèi)容
package main
import (
"fmt"
"io"
"os"
)
func main() {
// 讀取文件內(nèi)容
fileInfo, err := os.Open("./abc.go")
if err != nil {
panic(err)
}
contentBytes, err := io.ReadAll(fileInfo)
if err != nil {
panic(err)
}
fmt.Println(string(contentBytes))
}
此時(shí)讀取的 io stream
大小并不知道杆兵,io.ReadAll
使用什么策略讀取全部數(shù)據(jù)呢雁仲?滑動(dòng)窗口?線性/指數(shù)遞增讀人鲈唷攒砖?Talk is cheap. Show me the code
.
2. io.ReadAll Code
go1.16/src/io/io.go#L626
// 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)]
}
//println(cap(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
}
}
}
源碼解析:
從上面源碼可以看到,使用 make
先默認(rèn)申請(qǐng) cap = 512
的 []byte
日裙,然后進(jìn)入 for
循環(huán)迭代吹艇,直到數(shù)據(jù)全部讀取完成。for
循環(huán)中昂拂,首先通過(guò) len(b) == cap(b)
判斷 b
的容量是否滿了受神,如果已經(jīng)滿了,使用 append(b, 0)
追加一個(gè)元素政钟,此時(shí)會(huì)發(fā)生什么呢路克?
我們知道,一個(gè) slice
容量不夠了需要擴(kuò)容养交,但擴(kuò)容機(jī)制是怎樣的呢精算?繼續(xù) Show me the code
.
3. slice 擴(kuò)容機(jī)制
go1.16/src/runtime/slice.go#L125
// growslice handles slice growth during append.
// It is passed the slice element type, the old slice, and the desired new minimum capacity,
// and it returns a new slice with at least that capacity, with the old data
// copied into it.
// The new slice's length is set to the old slice's length,
// NOT to the new requested capacity.
// This is for codegen convenience. The old slice's length is used immediately
// to calculate where to write new values during an append.
// TODO: When the old backend is gone, reconsider this decision.
// The SSA backend might prefer the new length or to return only ptr/cap and save stack space.
func growslice(et *_type, old slice, cap int) slice {
...
newcap := old.cap
doublecap := newcap + newcap
//println("newcap: ", newcap)
//println("cap: ", cap)
if cap > doublecap {
newcap = cap
} else {
if old.cap < 1024 {
newcap = doublecap
} else {
// Check 0 < newcap to detect overflow
// and prevent an infinite loop.
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap <= 0 {
newcap = cap
}
}
}
...
}
源碼解析:
從上面源碼可以看到,slice
擴(kuò)容算法為:
1). 當(dāng)需要的容量(cap
)超過(guò)原切片容量的兩倍(doublecap
)時(shí)碎连,會(huì)使用需要的容量作為新容量(newcap
)灰羽;
2). 當(dāng)原切片容量 < 1024
時(shí),新切片的容量(newcap
)會(huì)直接翻倍(doublecap
)鱼辙;
3). 當(dāng)原切片容量 >= 1024
時(shí)廉嚼,會(huì)按原切片容量反復(fù)地增加 1/4
,直到新容量(newcap
)超過(guò)所需要的容量倒戏;
舉例說(shuō)明:
在上面 io.ReadAll
源碼中怠噪,初始 slice cap = 512
,后面擴(kuò)容將會(huì):
512
1024(doublecap)
1280(1024 + 1024/4)
1600(1280 + 1280/4)
2000(1600 + 1600/4)
...
實(shí)際擴(kuò)容 cap
是這樣的嗎杜跷?讓我們驗(yàn)證一下:
before newcap: 1024
-after newcap: 1024
before newcap: 1280
-after newcap: 1280
before newcap: 1600
-after newcap: 1792
before newcap: 2240
-after newcap: 2304
奇怪傍念?發(fā)現(xiàn) after newcap
并沒(méi)有按照上面預(yù)想的值擴(kuò)容矫夷,仔細(xì)挖代碼,發(fā)現(xiàn)除了按照上面 slice cap
擴(kuò)容外憋槐,還對(duì)內(nèi)存分配進(jìn)行了“對(duì)齊”:
go1.16/src/runtime/slice.go#L198
println("before newcap: ", newcap)
var overflow bool
var lenmem, newlenmem, capmem uintptr
// Specialize for common values of et.size.
// For 1 we don't need any division/multiplication.
// For sys.PtrSize, compiler will optimize division/multiplication into a shift by a constant.
// For powers of 2, use a variable shift.
switch {
...
case isPowerOfTwo(et.size):
var shift uintptr
if sys.PtrSize == 8 {
// Mask shift for better code generation.
shift = uintptr(sys.Ctz64(uint64(et.size))) & 63
} else {
shift = uintptr(sys.Ctz32(uint32(et.size))) & 31
}
lenmem = uintptr(old.len) << shift
newlenmem = uintptr(cap) << shift
capmem = roundupsize(uintptr(newcap) << shift) // 進(jìn)入到內(nèi)存塊(memory block)分配
overflow = uintptr(newcap) > (maxAlloc >> shift)
newcap = int(capmem >> shift)
...
}
println("after newcap: ", newcap)
進(jìn)入到內(nèi)存塊(memory block
)分配:
go1.16/src/runtime/msize.go#L13
// Returns size of the memory block that mallocgc will allocate if you ask for the size.
func roundupsize(size uintptr) uintptr {
if size < _MaxSmallSize {
if size <= smallSizeMax-8 {
return uintptr(class_to_size[size_to_class8[divRoundUp(size, smallSizeDiv)]])
} else {
return uintptr(class_to_size[size_to_class128[divRoundUp(size-smallSizeMax, largeSizeDiv)]])
}
}
if size+_PageSize < size {
return size
}
return alignUp(size, _PageSize)
}
獲取 spanClass
對(duì)應(yīng)的 size
:
go1.16/src/runtime/sizeclasses.go#L84
const (
_NumSizeClasses = 68
)
var class_to_size = [_NumSizeClasses]uint16{0, 8, 16, 24, 32, 48, 64, 80, 96, 112, 128,
144, 160, 176, 192, 208, 224, 240, 256, 288, 320, 352, 384, 416, 448, 480, 512, 576, 640,
704, 768, 896, 1024, 1152, 1280, 1408, 1536, 1792, 2048, 2304, 2688, 3072, 3200, 3456,
4096, 4864, 5376, 6144, 6528, 6784, 6912, 8192, 9472, 9728, 10240, 10880, 12288, 13568,
14336, 16384, 18432, 19072, 20480, 21760, 24576, 27264, 28672, 32768}
從上面 68
類 spanClass
可以看到双藕,我們想要分配 1600
被對(duì)齊到了 1792
,2240
被對(duì)齊到了 2304
阳仔,符合下面的驗(yàn)證結(jié)果:
before newcap: 1024
-after newcap: 1024
before newcap: 1280
-after newcap: 1280
before newcap: 1600
-after newcap: 1792
before newcap: 2240
-after newcap: 2304
4. 小結(jié)
從上面的源碼分析可以看到忧陪,io.ReadAll
通過(guò)使用 slice append
自動(dòng)擴(kuò)容 + 內(nèi)存對(duì)齊機(jī)制,使用增加的容量來(lái)實(shí)現(xiàn)對(duì) io stream
的全部讀取近范。slice append
擴(kuò)容算法為:
1). 當(dāng)需要的容量(cap
)超過(guò)原切片容量的兩倍(doublecap
)時(shí)嘶摊,會(huì)使用需要的容量作為新容量(newcap
);
2). 當(dāng)原切片容量 < 1024
時(shí)顺又,新切片的容量(newcap
)會(huì)直接翻倍(doublecap
)更卒;
3). 當(dāng)原切片容量 >= 1024
時(shí)等孵,會(huì)按原切片容量反復(fù)地增加 1/4
稚照,直到新容量(newcap
)超過(guò)所需要的容量;
后面將會(huì)有更多系列文章俯萌,解讀內(nèi)存分配果录、GC
機(jī)制、GPM
調(diào)度咐熙、面試系列弱恒、K8s
系列、etcd
系列等棋恼,如有錯(cuò)誤懇請(qǐng)指正返弹。最后,祝大家端午節(jié)快樂(lè)~