乐于分享
好东西不私藏

一起读Go源码(第10期) bytes:reader

一起读Go源码(第10期) bytes:reader

概述

友友们好啊,我们继续看看bytesreader源码。Reader实现的是只读字节流处理的组件,实现了对静态字节切片流式读取、随机定位、回退、批量写出的功能;我们就根据Reader最常用的流式读取和随机定位功能读读源码,首先来看看Reader结构体,注释的意思是Reader从一个字节切片读取数据,实现io.Reader、io.ReaderAt、io.WriteTo、io.Seeker、io.ByteScanner、io.RuneScanner接口。与Buffer不同,Reader是只读的,并支持随机定位。Reader的零值行为等价与一个空切片的读取。在Reader结构体中,有s []byte表示待读取的数据,i int64表示当前读指针的字节偏移量,prevRune int指的是上一个被读取的rune的起始下标,若未读取过rune则小于0。

// A Reader implements the [io.Reader], [io.ReaderAt], [io.WriterTo], [io.Seeker],// [io.ByteScanner], and [io.RuneScanner] interfaces by reading from// a byte slice.// Unlike a [Buffer], a Reader is read-only and supports seeking.// The zero value for Reader operates like a Reader of an empty slice.type Reader struct {    s        []byte    i        int64 // current reading index    prevRune int   // index of previous rune; or < 0}

流式读取与JSON解析

流式读取指的是将数据分批次、每次读取一点再读下一批,直到读完为止;跟一次性全部加载到内存的区别是,流式读取不会加载全部数据,内存中只会保留当前读取的那一批数据;读取过程是按顺序的(因此需要字节偏移量,记录当前读到哪了),最大的优点是内存占用很低且恒定

首先来看看流式读取的示例,data是一段准备好的字符串,buf是预定义好的8个字节缓冲区,用来放每次读出来的内容。然后for循环里用Read方法读取,读到EOF读完为止再跳出循环。最后打将内容打印出来。从打印结果可以看出来,Reader每次只读取8个字节,到末尾未读满,只读了3个字节。

data := []byte("abcdefghijklmnopqrsthdajsdhdcsakhjc")log.Println(len(data))r := bytes.NewReader(data)buf := make([]byte, 8)for {    n, err := r.Read(buf)    if err == io.EOF {        log.Println("读完了")        break    }    if err != nil {        log.Fatal(err)        return    }    log.Println("读取字节:", n, "内容:", string(buf[:n]))}2026/01/15 20:20:24 352026/01/15 20:20:24 读取字节: 8 内容: abcdefgh2026/01/15 20:20:24 读取字节: 8 内容: ijklmnop2026/01/15 20:20:24 读取字节: 8 内容: qrsthdaj2026/01/15 20:20:24 读取字节: 8 内容: sdhdcsak2026/01/15 20:20:24 读取字节: 3 内容: hjc2026/01/15 20:20:24 读完了

基于上面的示例我们看Read源码看看是怎么实现的:

  1. 1. 做边界判断,如果i索引已经大于等于切片长度,说明数据已经读完了,返回io.EOF
  2. 2. r.prevRune=-1,将上一个Rune字符位置复位
  3. 3. n = copy(b,r.s[r.i:]),看一下copy的注释,内置函数将源切片(src) 的元素复制到目标切片(dst)中,它也支持将 字符串(string) 的字节复制到字节切片([]byte)中,源和目标的内存空间可以重叠(不影响拷贝结果);copy返回实际被拷贝的元素数量,这个数量永远 = len(dst)和len(src)的最小值
  4. 4. 更新读取的索引,下次调用Read方法时继续从这开始读。
// Read implements the [io.Reader] interface.func (r *Reader) Read(b []byte) (n int, err error) {    if r.i >= int64(len(r.s)) {        return 0, io.EOF    }    r.prevRune = -1    n = copy(b, r.s[r.i:])    r.i += int64(n)    return}// The copy built-in function copies elements from a source slice into a// destination slice. (As a special case, it also will copy bytes from a// string to a slice of bytes.) The source and destination may overlap. Copy// returns the number of elements copied, which will be the minimum of// len(src) and len(dst).func copy(dst, src []Type) int

理解了什么叫流式读取,那Reader自然就能读取非常非常非常大的字符串切片,如下是流式读取json字符串功能示例:

type Person struct {    Name string `json:"name"`    Car  string `json:"car"`    Num  int    `json:"num"`}...bigJsonBytes := []byte(`[    {"name":"Max","car":"rb21","num":1},    {"name":"Leclerc","car":"sf25","num":16},    {"name":"Norris","car":"mcl39","num":4}]`)reader := bytes.NewReader(bigJsonBytes)decoder := json.NewDecoder(reader)token, err := decoder.Token()if err != nil {    log.Println(err)    return}if token != json.Delim('[') {    log.Println("JSON数据格式错误")    return}var p Personindex := 0for decoder.More() {    if err := decoder.Decode(&p); err != nil {        log.Println("解析元素失败:", index, err)        break    }    log.Println("解析数据", index, p)    index++}token, err = decoder.Token()if err != nil && err != io.EOF {    log.Println(err)    return}if token != json.Delim(']') {    log.Println("json数据有误")    return}log.Println("读完了")2026/01/15 20:26:23 解析数据 0 {Max rb21 1}2026/01/15 20:26:23 解析数据 1 {Leclerc sf25 16}2026/01/15 20:26:23 解析数据 2 {Norris mcl39 4}2026/01/15 20:26:23 读完了

随机定位读取

在reader中随机定位读取有两种方式,分别是ReadAtSeek

  • • ReadAt方法:传入b []byte表示字节缓冲区,用于接收本次读取到的数据;off int64表示读取的偏移量。返回n表示成功读取到的字节数和错误error。因为调用ReadAt不会修内部游标因此多次调用互相不会干扰
  • • Seek方法:传入offset int64是相对偏移量,whence int是偏移定位,有SeekStart(数据源开头),SeekCurrent(内部游标位置),SeekEnd(数据源结尾)这三种,下文源码会看到。返回参数表示内部游标位置和错误。Seek会修改内部游标的值,适合先定位到某个位置再连续读取的场景。

当然了Seek方法单纯只定位,要配合Read方法才能读取,可以看看下面ReadAt和Seek的简单示例感受一下:

data := []byte("abcdefghijklmnopq")log.Println(len(data))r1 := bytes.NewReader(data)//  从开头偏移,读取指定长度newPos, _ := r1.Seek(2, io.SeekStart)log.Println("seek偏移后指针位置:", newPos)buf1 := make([]byte, 2)n1, _ := r1.Read(buf1)log.Println("读取内容与字节数:", string(buf1[:n1]), n1)log.Println("Seek操作后r1的内部游标", r1.Size()-int64(r1.Len()))r2 := bytes.NewReader(data)buf2 := make([]byte, 3)n2, _ := r2.ReadAt(buf2, 6)log.Println("读取内容与字节数", string(buf2[:n2]), n2)log.Println("ReadAt操作后r2的内部游标", r2.Size()-int64(r2.Len()))2026/01/15 21:02:07 172026/01/15 21:02:07 seek偏移后指针位置: 22026/01/15 21:02:07 读取内容与字节数: cd 22026/01/15 21:02:07 Seek操作后r1的内部游标 42026/01/15 21:02:07 读取内容与字节数 ghi 32026/01/15 21:02:07 ReadAt操作后r2的内部游标 0

那Seek究竟是怎么实现的呢?

  • • Seek 方法:是bytes.Reader对标准口io.Seeker接口的实现。
    1. 1. r.prevRune = -1 重置上一个字符读取标记,声明abs变量用于存储游标位置;
    2. 2. 根据whence判断如何计算游标位置,有SeekStart从数据源初始位置、SeekCurrent从当前游标位置和SeekEnd从数据源最后一个字节的下一位置。如果传入的whence不属于以上3个值就返回报错。
    3. 3. 判断abs是否大于0,否则返回报错。
    4. 4. r.i = abs,完成游标位置的修改。返回游标
// Seek implements the [io.Seeker] interface.func (r *Reader) Seek(offset int64, whence int) (int64, error) {    r.prevRune = -1    var abs int64    switch whence {    case io.SeekStart:        abs = offset    case io.SeekCurrent:        abs = r.i + offset    case io.SeekEnd:        abs = int64(len(r.s)) + offset    default:        return 0, errors.New("bytes.Reader.Seek: invalid whence")    }    if abs < 0 {        return 0, errors.New("bytes.Reader.Seek: negative position")    }    r.i = abs    return abs, nil}

回头看上面的Read函数源码,n = copy(b, r.s[r.i:]),读取会从当前游标开始,一套动作下来实现了定位+读取功能。而ReadAt方法围绕着只读取,不修改内部游标的逻辑实现:

  1. 1. 边界判断,off便宜量要大于0且不能比原数据总长度还长(否则认定在末尾,返回已读完)
  2. 2. n = copy(b, r.s[off:]),从传入的偏移量开始读
  3. 3. 若已读到数据长度小于传入缓冲区长度b,说明本次读取已经到末尾,返回err为io.EOF(读完了)
// ReadAt implements the [io.ReaderAt] interface.func (r *Reader) ReadAt(b []byte, off int64) (n int, err error) {    // cannot modify state - see io.ReaderAt    if off < 0 {        return 0, errors.New("bytes.Reader.ReadAt: negative offset")    }    if off >= int64(len(r.s)) {        return 0, io.EOF    }    n = copy(b, r.s[off:])    if n < len(b) {        err = io.EOF    }    return}

写在最后

本人是新手小白,如果这篇笔记中有任何错误或不准确之处,真诚地希望各位读者能够给予批评和指正,如有更好的实现方法请给我留言,谢谢!欢迎大家在评论区留言!觉得写得还不错的话欢迎大家关注一波!下一篇我们继续看bytes。

本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » 一起读Go源码(第10期) bytes:reader

评论 抢沙发

6 + 5 =
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
×
订阅图标按钮