一起读Go源码(第10期) bytes:reader
概述
友友们好啊,我们继续看看bytes的reader源码。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. 做边界判断,如果i索引已经大于等于切片长度,说明数据已经读完了,返回 io.EOF -
2. r.prevRune=-1,将上一个Rune字符位置复位 -
3. n = copy(b,r.s[r.i:]),看一下copy的注释,内置函数将源切片(src) 的元素复制到目标切片(dst)中,它也支持将 字符串(string) 的字节复制到字节切片([]byte)中,源和目标的内存空间可以重叠(不影响拷贝结果);copy返回实际被拷贝的元素数量,这个数量永远 = len(dst)和len(src)的最小值。 -
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中随机定位读取有两种方式,分别是ReadAt和Seek;
-
• 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. r.prevRune = -1重置上一个字符读取标记,声明abs变量用于存储游标位置; -
2. 根据 whence判断如何计算游标位置,有SeekStart从数据源初始位置、SeekCurrent从当前游标位置和SeekEnd从数据源最后一个字节的下一位置。如果传入的whence不属于以上3个值就返回报错。 -
3. 判断abs是否大于0,否则返回报错。 -
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. 边界判断,off便宜量要大于0且不能比原数据总长度还长(否则认定在末尾,返回已读完) -
2. n = copy(b, r.s[off:]),从传入的偏移量开始读 -
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。
夜雨聆风
