Go 面试系列(五) - io.ReadAll 怎样读全部?

在进行本地 file 文件内容读取,或进行 HTTP 网络接口通信的时候,我们经常使用 io.ReadAll 来读取远程接口返回的 resp.Body,但接口返回数据量有大有小,io.ReadAll 是怎样完成全部数据的读取的?

带着此疑问,让我们走近 io.ReadAll 源码一探究竟:

1. Demo 读取文件内容

package main

import (
    "fmt"
    "io"
    "os"
)

func main() {
    // 读取文件内容
    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))
}

此时读取的 io stream 大小并不知道,io.ReadAll 使用什么策略读取全部数据呢?滑动窗口?线性/指数递增读取?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 先默认申请 cap = 512[]byte,然后进入 for 循环迭代,直到数据全部读取完成。for 循环中,首先通过 len(b) == cap(b) 判断 b 的容量是否满了,如果已经满了,使用 append(b, 0) 追加一个元素,此时会发生什么呢?

我们知道,一个 slice 容量不够了需要扩容,但扩容机制是怎样的呢?继续 Show me the code.

3. slice 扩容机制

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 扩容算法为:
1). 当需要的容量(cap)超过原切片容量的两倍(doublecap)时,会使用需要的容量作为新容量(newcap);
2). 当原切片容量 < 1024 时,新切片的容量(newcap)会直接翻倍(doublecap);
3). 当原切片容量 >= 1024 时,会按原切片容量反复地增加 1/4,直到新容量(newcap)超过所需要的容量;

举例说明:
在上面 io.ReadAll 源码中,初始 slice cap = 512,后面扩容将会:

512
1024(doublecap)
1280(1024 + 1024/4)
1600(1280 + 1280/4)
2000(1600 + 1600/4)
...

实际扩容 cap 是这样的吗?让我们验证一下:

before newcap:  1024
-after newcap:  1024
before newcap:  1280
-after newcap:  1280
before newcap:  1600
-after newcap:  1792
before newcap:  2240
-after newcap:  2304

奇怪?发现 after newcap 并没有按照上面预想的值扩容,仔细挖代码,发现除了按照上面 slice cap扩容外,还对内存分配进行了“对齐”:

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) // 进入到内存块(memory block)分配
        overflow = uintptr(newcap) > (maxAlloc >> shift)
        newcap = int(capmem >> shift)
    ...
    }

    println("after newcap: ", newcap)

进入到内存块(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 对应的 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 被对齐到了 17922240 被对齐到了 2304,符合下面的验证结果:

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. 小结

从上面的源码分析可以看到,io.ReadAll 通过使用 slice append 自动扩容 + 内存对齐机制,使用增加的容量来实现对 io stream 的全部读取。slice append 扩容算法为:
1). 当需要的容量(cap)超过原切片容量的两倍(doublecap)时,会使用需要的容量作为新容量(newcap);
2). 当原切片容量 < 1024 时,新切片的容量(newcap)会直接翻倍(doublecap);
3). 当原切片容量 >= 1024 时,会按原切片容量反复地增加 1/4,直到新容量(newcap)超过所需要的容量;

后面将会有更多系列文章,解读内存分配、GC 机制、GPM 调度、面试系列、K8s 系列、etcd 系列等,如有错误恳请指正。最后,祝大家端午节快乐~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,137评论 6 511
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,824评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,465评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,131评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,140评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,895评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,535评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,435评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,952评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,081评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,210评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,896评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,552评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,089评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,198评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,531评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,209评论 2 357

推荐阅读更多精彩内容

  • array 和 slice 看似相似,却有着极大的不同,但他们之间还有着千次万缕的联系 slice 是引用类型、是...
    戚银阅读 982评论 1 4
  • 原文:【http://alblue.cn/articles/2020/07/04/1593837537036.ht...
    98k_sw阅读 11,680评论 1 4
  • 简介 切片(slice)是 Go 语言提供的一种数据结构,使用非常简单、便捷。但是由于实现层面的原因,切片也经常会...
    darjun阅读 352评论 0 0
  • 我是黑夜里大雨纷飞的人啊 1 “又到一年六月,有人笑有人哭,有人欢乐有人忧愁,有人惊喜有人失落,有的觉得收获满满有...
    陌忘宇阅读 8,538评论 28 53
  • 信任包括信任自己和信任他人 很多时候,很多事情,失败、遗憾、错过,源于不自信,不信任他人 觉得自己做不成,别人做不...
    吴氵晃阅读 6,190评论 4 8