Strings.Reader源码详解

你必须非常努力,才能看起来毫不费力!

微信搜索公众号[ 漫漫Coding路 ],一起From Zero To Hero !

前言

前面三篇文章 Go语言 io包核心接口详解Go语言 io包基本接口详解Go语言 io包源码解读,我们学习了Go语言 io 包中的接口和方法,本篇文章我们就来学习下具体的实现-strings.Reader。strings.Reader 用于高效的读取字符串,实现了 io.Reader、io.ReaderAt、io.Seeker、io.WriterTo、io.ByteScanner、io.RuneScanner 接口,下面我们就通过阅读源码来学习下底层实现吧!

定义

  • s: 初始化传入的字符串,后续的操作都是对该字符串的操作
  • i: 已读计数,保存下次读取的开始位置,默认是0(第一次读取就是从0开始),读取n个byte后,会相应的加n。
  • prevRune: 保存上一个Rune的位置,默认是-1,只有在调用 ReadRune 方法的时候才会增加
type Reader struct {
    s        string
    i        int64 
    prevRune int   
}

方法定义

NewReader

初始化一个 Strings.Reader,传入需要操作的字符串,初始化当前读取位置 i=0,prevRune=-1

func NewReader(s string) *Reader { return &Reader{s, 0, -1} }

Len

Len 方法返回 未读取 部分的长度

func (r *Reader) Len() int {
    // 如果当前读取开始位置 i 大于等于 字符串 的长度,说明之前已经读取完了字符串,此时返回0
    if r.i >= int64(len(r.s)) {
        return 0
    }
    // 返回剩余长度
    return int(int64(len(r.s)) - r.i)
}

Size

Size 方法返回原始字符串的长度,每次调用该方法的返回值都相同,不受其他方法的影响;利用 Size()-Len(),可以计算出已经读取的字节数

func (r *Reader) Size() int64 { return int64(len(r.s)) }

Read

Read 方法读取字符串到字节切片 b 中,返回读取的字节长度和产生的 error

// Read 方法读取字符串到字节切片 b 中,返回读取的字节长度和产生的 error
func (r *Reader) Read(b []byte) (n int, err error) {

    // 如果当前读取开始位置,已经大于等于字符串长度,说明已经对字符串读取完毕了,返回 EOF error
    if r.i >= int64(len(r.s)) {
        return 0, io.EOF
    }

    // 只有在调用 ReadRune 方法的时候才会修改,如果调用其他方法都会设置为默认值
    r.prevRune = -1

    // 调用 copy 方法,从当前位置 i 开始,将字符串 s 中的数据拷贝至字节切片 b 中,返回拷贝的字节数 n
    n = copy(b, r.s[r.i:])

    // 当前读取的位置往后移动 n 个
    r.i += int64(n)
    return
}

ReadAt

  • ReadAt 从指定位置 off 开始读取字符串至字节切片 b 中,返回读取到的字节数 n 以及产生的 error
  • ReadAt 不会修改已读计数 i,也不会修改 prevRune 值
func (r *Reader) ReadAt(b []byte, off int64) (n int, err error) {
    // 如果传入的 off < 0,是非法偏移量
    if off < 0 {
        return 0, errors.New("strings.Reader.ReadAt: negative offset")
    }

    // 如果传入的 off 大于等于 字符串的长度,那么就没有数据可以读取了,返回 EOF error
    if off >= int64(len(r.s)) {
        return 0, io.EOF
    }

    // 其他位置就是合法位置
    // 从 off 位置开始,调用 copy 方法,复制数据到字节切片 b 中,返回复制的字节数
    n = copy(b, r.s[off:])

    // 根据 ReaderAt 接口的定义,如果读取的字节长度小于传入字节切片的长度,返回的 err 不能为 nil,需要说明原因
    // 如果复制的字节数小于字节切片 b 的长度,说明已经复制到了字符串结尾,不够填充满字节切片b 了,返回 EOF error
    if n < len(b) {
        err = io.EOF
    }
    return
}

ReadByte

读取一个字节,返回读取的字节和产生的error

func (r *Reader) ReadByte() (byte, error) 
    // 只有在调用 ReadRune 方法的时候才会修改,如果调用其他方法都会设置为默认值
    r.prevRune = -1

    // 如果当前读取开始位置,大于等于字符串长度,说明已经读取完毕了,返回 EOF error
    if r.i >= int64(len(r.s)) {
        return 0, io.EOF
    }
    // 读取当前开始位置的字节,然后将 i+1
    b := r.s[r.i]
    r.i++
    return b, nil
}

UnreadByte

回退一个字节,将已读计数的值减一

func (r *Reader) UnreadByte() error {
    // 如果当前开始位置 i <= 0,再减一非法了,返回 error
    if r.i <= 0 {
        return errors.New("strings.Reader.UnreadByte: at beginning of string")
    }

    // 只有在调用 ReadRune 方法的时候才会修改,如果调用其他方法都会设置为默认值
    r.prevRune = -1

    // 回退一个字节
    r.i--
    return nil
}

ReadRune

  • ReadRune 作用是读取一个 UTF-8 字符,返回读取的 rune、字节数 以及 产生的 error
  • rune 不同于字节,而是一个 UTF-8 字符,比如中文 "我" 是三个字节,英文字母 'a' 是一个字节,而两者都可以表示为一个rune(UTF-8 字符)
func (r *Reader) ReadRune() (ch rune, size int, err error) {
    // 如果当前开始位置 i 已经大于等于字符串长度,已经没有数据可以读取,返回 EOF error
    if r.i >= int64(len(r.s)) {
        r.prevRune = -1
        return 0, 0, io.EOF
    }

    // 如果当前位置合法,设置prevRune为当前 i 的位置,然后进行读取。
    r.prevRune = int(r.i)

    // 判断当前 i 位置的字节值是否小于utf8.RuneSelf
  // 小于表示当前的字节就是一个独立的UTF-8 字符,那么返回这个字节对应的数据即可
    if c := r.s[r.i]; c < utf8.RuneSelf {
        // 本次只读取了一个字节,i+1
        r.i++
        return rune(c), 1, nil
    }

    // DecodeRuneInString 从字符串指定位置开始,返回从该位置开始的一个完整的 rune,以及对应的字节数
    // 从当前 i 位置开始,调用 DecodeRuneInString 方法,并更新 i 值
    ch, size = utf8.DecodeRuneInString(r.s[r.i:])
    r.i += int64(size)
    return
}

UnreadRune

  • UnreadRune表示回退一个 rune,,而且只能回退一次
  • 这个方法只有在运行过 ReadRune 后,且没有运行 Read、ReadByte、UnreadByte、Seek、WriteTo方法时调用才有效(这些方法会将 prevRune 更新为默认值)
  • 需要注意的是,ReadRune调用后,运行 ReadAt 方法不改变 prevRune 的状态,可以使用 UnreadRune 回退
func (r *Reader) UnreadRune() error {
    // 未操作过字符串,不能回退
    if r.i <= 0 {
        return errors.New("strings.Reader.UnreadRune: at beginning of string")
    }
    // 非法位置,无法回退
    if r.prevRune < 0 {
        return errors.New("strings.Reader.UnreadRune: previous operation was not ReadRune")
    }
    // 修改已读计数 i 为 prevRune
    r.i = int64(r.prevRune)

    // prevRune 设置为默认值,不能再次回退了
    r.prevRune = -1
    return nil
}

Seek

Seek 基于 起始位置 whence 和 偏移量 offset,返回新的位置和产生的 error,同时也会修改已读计数 i,设定下一次读取的起始位置

func (r *Reader) Seek(offset int64, whence int) (int64, error) {
    r.prevRune = -1
    var abs int64
    switch whence {
        // 从开始位置开始,也就是从0开始,那么就设置为入参 offset
    case io.SeekStart:
        abs = offset
        //从当前位置开始,也就是从 i 开始,设置为 i + offset
    case io.SeekCurrent:
        abs = r.i + offset
        // 从末尾开始,也就是从字符串默认位置开始
    case io.SeekEnd:
        abs = int64(len(r.s)) + offset
    default:
        // 其他 whence 入参非法
        return 0, errors.New("strings.Reader.Seek: invalid whence")
    }
    // 根据Seeker接口的定义,如果新的 offset 在文件开始位置之前,是非法的,需要返回 err!=nil
    if abs < 0 {
        return 0, errors.New("strings.Reader.Seek: negative position")
    }
    // 更新位置
    r.i = abs
    return abs, nil
}

WriteTo

WriteTo方法,将从已读计数 i 开始的数据,交给 Writer w 写入,返回写入的字节数和产生的error

func (r *Reader) WriteTo(w io.Writer) (n int64, err error) {
    // 将 prevRune 设置为默认值
    r.prevRune = -1

    // 如果当前开始位置 i 大于等于 字符串长度,没有数据可写
    if r.i >= int64(len(r.s)) {
        return 0, nil
    }
    // 从 i 位置开始的数据,调用 io.WriteString 方法,交给 Writer w 写入,返回写入的字节数和err
    s := r.s[r.i:]
    m, err := io.WriteString(w, s)

    // 如果写入的字节数,比入参字符串长度还长,不合理,直接panic
    if m > len(s) {
        panic("strings.Reader.WriteTo: invalid WriteString count")
    }

    // 被读取了 m 个字节,更新 i 位置
    r.i += int64(m)

    /* 
    如果写入的字符,比传递字符串的长度不相等,且 err==nil,返回 ErrShortWrite
    
    分析:
    
    上面调用的 WriteString(w Writer, s string)方法,该方法定义如下:
    
    func WriteString(w Writer, s string) (n int, err error) {
        if sw, ok := w.(StringWriter); ok {
            return sw.WriteString(s)
        }
        return w.Write([]byte(s))
    }
    
    1.根据 WriteString(w Writer, s string)的逻辑,如果传入的 Writer w 实现了StringWriter接口,
    进一步会直接调用StringWriter.WriteString方法,该方法没有约束返回的写入字节数、产生的error 与入参字符串之间的规范

    2. 如果传入的 Writer w 没有实现StringWriter接口,会直接调用Writer.Write 方法
    Write 方法明确说明:如果写入的字节数小于传入的字节数,err 一定不为 nil

    因此这里的 m != len(s) && err == nil , 一定是第一种情形返回的
    */
    n = int64(m)
    if m != len(s) && err == nil {
        err = io.ErrShortWrite
    }
    return
}

Reset

重置:将整个结构初始化为最初状态,设置新的字符串

func (r *Reader) Reset(s string) { *r = Reader{s, 0, -1} }

测试使用

func main() {
    reader := strings.NewReader("this is a test")
    fmt.Println(reader.Size()) // 字符串长度  14
    fmt.Println(reader.Len())  // 未读字节数  14

    b := make([]byte, 10)
    n, err := reader.Read(b)           // 读取10个字节
    fmt.Println(n, err)                // 10 <nil>
    fmt.Println("'" + string(b) + "'") // 'this is a '

    // 当前 i=10
    t, err := reader.ReadByte()
    fmt.Println(string(t), err) // t <nil>
    _ = reader.UnreadByte()     // 回退一个字节
    t, err = reader.ReadByte()
    fmt.Println(string(t), err) // t <nil>

    offset, err := reader.Seek(5, io.SeekStart) // 设置 i=5
    b = make([]byte, 10)
    n, err = reader.ReadAt(b, offset)  // 14-5,读取9个字节
    fmt.Println(n, err)                // 9 EOF
    fmt.Println("'" + string(b) + "'") // 'is a test'

    r, s, err := reader.ReadRune() // 从 i=5,读取一个 rune
    fmt.Println(string(r), s, err) // i 1 <nil>

    reader.Reset("这是一个测试") // reset i=0
    r, s, err = reader.ReadRune()
    fmt.Println(string(r), s, err) // 这 3 <nil>
    _ = reader.UnreadRune()        // 回退一个rune
    r, s, err = reader.ReadRune()
    fmt.Println(string(r), s, err) //这 3 <nil>
}

总结

本篇文章学习了strings.Reader,对涉及到的所有方法进行了源码级别的分析,并给出了使用示例。在strings.Reader中,比较重要的变量就是已读计数 i 了,下面总结了几条该字段何时会被更新:

  • Reader 拥有的大部分用于读取的方法都会及时地更新已读计数
  • ReadByte 方法会在读取成功后将这个计数的值加 1,ReadRune方法在读取成功之后,会把被读取的字符所占用的字节数作为计数的增量
  • ReadAt方法算是一个例外,它既不会依据已读计数进行读取,也不会在读取后更新它。正因为如此,这个方法可以自由地读取其所属的Reader值中的任何内容
  • Reader值的 Seek 方法也会更新该值的已读计数,实际上,这个Seek方法的主要作用正是设定下一次读取的起始索引位置

更多

个人博客: https://lifelmy.github.io/

微信公众号:漫漫Coding路

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

推荐阅读更多精彩内容