你必须非常努力,才能看起来毫不费力!
微信搜索公众号[ 漫漫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路