Go 面試系列(五) - io.ReadAll 怎樣讀全部冷离?

在進(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}

從上面 68spanClass 可以看到双藕,我們想要分配 1600 被對(duì)齊到了 17922240 被對(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è)~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末爪飘,一起剝皮案震驚了整個(gè)濱河市义起,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌师崎,老刑警劉巖默终,帶你破解...
    沈念sama閱讀 216,496評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異犁罩,居然都是意外死亡齐蔽,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)床估,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)含滴,“玉大人,你說(shuō)我怎么就攤上這事丐巫√缚觯” “怎么了源哩?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,632評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)鸦做。 經(jīng)常有香客問(wèn)我励烦,道長(zhǎng),這世上最難降的妖魔是什么泼诱? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,180評(píng)論 1 292
  • 正文 為了忘掉前任坛掠,我火速辦了婚禮,結(jié)果婚禮上治筒,老公的妹妹穿的比我還像新娘屉栓。我一直安慰自己,他們只是感情好耸袜,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,198評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布友多。 她就那樣靜靜地躺著,像睡著了一般堤框。 火紅的嫁衣襯著肌膚如雪域滥。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,165評(píng)論 1 299
  • 那天蜈抓,我揣著相機(jī)與錄音启绰,去河邊找鬼。 笑死沟使,一個(gè)胖子當(dāng)著我的面吹牛委可,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播腊嗡,決...
    沈念sama閱讀 40,052評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼着倾,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了燕少?” 一聲冷哼從身側(cè)響起卡者,我...
    開(kāi)封第一講書(shū)人閱讀 38,910評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎棺亭,沒(méi)想到半個(gè)月后虎眨,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,324評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡镶摘,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,542評(píng)論 2 332
  • 正文 我和宋清朗相戀三年嗽桩,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片凄敢。...
    茶點(diǎn)故事閱讀 39,711評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡碌冶,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出涝缝,到底是詐尸還是另有隱情扑庞,我是刑警寧澤譬重,帶...
    沈念sama閱讀 35,424評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站罐氨,受9級(jí)特大地震影響臀规,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜栅隐,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,017評(píng)論 3 326
  • 文/蒙蒙 一塔嬉、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧租悄,春花似錦谨究、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,668評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至潭辈,卻和暖如春鸯屿,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背萎胰。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,823評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工碾盟, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人技竟。 一個(gè)月前我還...
    沈念sama閱讀 47,722評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像屈藐,于是被迫代替她去往敵國(guó)和親榔组。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,611評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容

  • array 和 slice 看似相似联逻,卻有著極大的不同搓扯,但他們之間還有著千次萬(wàn)縷的聯(lián)系 slice 是引用類型、是...
    戚銀閱讀 980評(píng)論 1 4
  • 原文:【http://alblue.cn/articles/2020/07/04/1593837537036.ht...
    98k_sw閱讀 11,655評(píng)論 1 4
  • 簡(jiǎn)介 切片(slice)是 Go 語(yǔ)言提供的一種數(shù)據(jù)結(jié)構(gòu)包归,使用非常簡(jiǎn)單锨推、便捷。但是由于實(shí)現(xiàn)層面的原因公壤,切片也經(jīng)常會(huì)...
    darjun閱讀 348評(píng)論 0 0
  • 我是黑夜里大雨紛飛的人啊 1 “又到一年六月换可,有人笑有人哭,有人歡樂(lè)有人憂愁厦幅,有人驚喜有人失落沾鳄,有的覺(jué)得收獲滿滿有...
    陌忘宇閱讀 8,535評(píng)論 28 53
  • 信任包括信任自己和信任他人 很多時(shí)候,很多事情确憨,失敗译荞、遺憾瓤的、錯(cuò)過(guò),源于不自信吞歼,不信任他人 覺(jué)得自己做不成圈膏,別人做不...
    吳氵晃閱讀 6,187評(píng)論 4 8