Go语言 strings.Reader 源码详解

1,033 阅读6分钟

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

微信搜索公众号[ 漫漫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方法的主要作用正是设定下一次读取的起始索引位置

更多

个人博客: lifelmy.github.io/

微信公众号:漫漫Coding路