Go语言 io包源码解读

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

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

前言

前两篇文章 Go语言 io包核心接口详解Go语言 io包基本接口详解,我们已经学习了 io包 中的核心接口、基本接口 和 组合接口,这些都是基本的接口定义和规范,那么本篇文章我们就一起来看下io包 中对上述接口的使用,包括三个结构体和部分方法,我们通过研究源码来加深对接口定义的理解。

结构体

LimitedReader

LimitedReader 限制读取的数据长度,至多读取 n 个字节,具体定义如下:

// Reader R 作为底层的reader,用于读取数据;
// N 记录还剩余多少字节可以读取(初始化为n,每次读取一次数据之后,会更新剩余可读字节数)
type LimitedReader struct {
    R Reader // underlying reader
    N int64  // max bytes remaining
}

相关方法

  1. 返回 LimitedReader 实例
// 返回LimitReader 实例,r 作为底层 Reader,n 为限制读取的字节数大小
func LimitReader(r Reader, n int64) Reader { return &LimitedReader{r, n} }
  1. Read 方法
// Read 方法读取数据到字节数组p中,返回读取的字节长度 和 产生的 error
// 每次调用该方法都会更新剩余可读字节数N
func (l *LimitedReader) Read(p []byte) (n int, err error) {

    // 如果剩余可读字节数 N<=0,返回 EOF error
    if l.N <= 0 {
        return 0, EOF
    }

    // 如果提供的字节数组空间过大,只需要使用 N 个长度即可,因为限制了至多读取读取 N 个字节
    if int64(len(p)) > l.N {
        p = p[0:l.N]
    }

    // 读取数据到字节数据 p,并将 N 减去成功读取的字节数
    n, err = l.R.Read(p)
    l.N -= int64(n)
    return
}

SectionReader

SectionReader 包装了 ReaderAt 类型,重写了 Read、Seek和 ReadAt 方法。SectionReader 的作用是限制了读取数据的范围,只能够读取原始数据中的某一个部分(或者说某一段)。定义如下:

// r:ReaderAt 实例,用于读取数据
// base:保存可读取数据范围的起始位置,变量值不会变
// off:保存当前位于的位置,每读取一次数据,变量值会发生改变
// limit:保存可读取数据范围的结束位置,变量值不会变
type SectionReader struct {
    r     ReaderAt
    base  int64
    off   int64
    limit int64
}

相关方法

  1. 初始化 SectionReader 实例
// NewSectionReader 返回初始化后的 SectionReader,需要提供一个 ReaderAt 实例,以及读取的起始位置 off 和 需要读取的数据长度 n,也就是限制了读取的数据范围为[off,off+n)
func NewSectionReader(r ReaderAt, off int64, n int64) *SectionReader {
    return &SectionReader{r, off, off, off + n}
}
  1. Read 方法:在指定的范围内读取数据
// 读取数据到字节数组p中,返回读取的字节长度和产生的error
func (s *SectionReader) Read(p []byte) (n int, err error) {

    // 如果当前处于的位置,超出了可读取数据范围,返回 EOF error
    if s.off >= s.limit {
        return 0, EOF
    }

    // 如果字节数组p的长度大于 剩余可读取数据长度,将p的长度缩小到剩余可读取的数据的长度
    if max := s.limit - s.off; int64(len(p)) > max {
        p = p[0:max]
    }

    // 从 off 位置开始读取数据,并更新 off 的位置
    n, err = s.r.ReadAt(p, s.off)
    s.off += int64(n)
    return
}
  1. Seek 方法:根据提供的 whence 和 offset, 设置 SectionReader off 变量的值
var errWhence = errors.New("Seek: invalid whence")
var errOffset = errors.New("Seek: invalid offset")

// Seek 方法 根据 whence 和 offset, 设置 SectionReader off 变量的值,返回距可读范围起始位置的长度和产生的error
func (s *SectionReader) Seek(offset int64, whence int) (int64, error) {
    switch whence {
    default:
        return 0, errWhence
    // 从起始位置开始seek,即基于SectionReader.base
    case SeekStart:
        offset += s.base
    // 从当前位置开始seek,即基于SectionReader.off
    case SeekCurrent:
        offset += s.off
    // 从当前位置开始seek,即基于SectionReader.limit
    case SeekEnd:
        offset += s.limit
    }

    // 如果最终offset的位置在 base 之前,则是非法位置,会返回error;
  // 如果在limit之后,没有返回error,可以参考 Seeker 接口中对 Seek 方法的定义
    if offset < s.base {
        return 0, errOffset
    }

    // 修改 off 的值,返回距离 base 的长度
    s.off = offset
    return offset - s.base, nil
}
  1. ReadAt 方法:基于起始位置base,根据 入参off 计算读取位置偏移量为 base+off, 然后从该位置开始,读取可读数据范围内的数据到字节数组p中。(和 SectionReader 结构体中定义的 off 变量无关)
func (s *SectionReader) ReadAt(p []byte, off int64) (n int, err error) {
    // 如果入参 off<0 或者 off 大小超出了可读数据范围,返回 EOF error
    if off < 0 || off >= s.limit-s.base {
        return 0, EOF
    }

    // 本次读取的起始位置为 off + s.base
    off += s.base

    // 如果字节数组 p 的长度大于可读取长度 s.limit-off,那么将字节数组缩小到可读取长度大小
    if max := s.limit - off; int64(len(p)) > max {
        p = p[0:max]
        n, err = s.r.ReadAt(p, off)
        // 由于读取的数据长度,小于原始字节数组 p的长度 len(p),参照 ReaderAt接口中对ReadAt方法的定义,需要返回error
        if err == nil {
            err = EOF
        }
        return n, err
    }
    // 如果字节数组的长度小于等于可读取长度 s.limit-off,读取数据到 p 中
    return s.r.ReadAt(p, off)
}
  1. Size 方法:返回可读取数据的范围大小

    // Size 返回可读范围的长度
    func (s *SectionReader) Size() int64 { return s.limit - s.base }
    

teeReader

teeReader 是一个包级私有的数据类型,因为其首字母是小写。teeReader 的作用就是将 Reader r 读取的数据,交给 Writer r 去写入到文件中,相当于提供了一个桥梁,连接起来了一个 Reader 和 Writer。teeReader 会将 Reader 中读取到的所有数据,一次性交给 Writer,没有缓冲区。定义如下:

type teeReader struct {
   r Reader
   w Writer
}

相关方法

  1. 初始化方法:提供一个Reader 和 Writer 用于初始化
func TeeReader(r Reader, w Writer) Reader {
    return &teeReader{r, w}
}
  1. Read 方法:Reader r 将数据读取到字节数组 p 中,然后 Writer 将 p 中的数据写入到文件中,p 充当了数据的中转站。
func (t *teeReader) Read(p []byte) (n int, err error) {
  // 读取数据到字节数组 p 中,返回读取的数据长度 和 error
    n, err = t.r.Read(p)
  
  // 根据 Reader 接口中对 Read 方法的定义,调用者应该首先关注 n 是否大于0,而不是应该首先关注 err
  // 如果 n>0,说明读取到了数据,则将 p 中读取到的数据部分 p[:n] 交给 Writer w 去写入
    if n > 0 {
        if n, err := t.w.Write(p[:n]); err != nil {
            return n, err
        }
    }
    return
}

方法定义

WriteString

WriteString 用于将字符串写入文件

// 入参: Writer w,用于将字符串写入文件; s: 需要写入的字符串
// 返回值: 写入成功的字节数n,可能产生的error

func WriteString(w Writer, s string) (n int, err error) {
  // 如果传入的 writer 实现了 StringWriter 接口,则直接调用 StringWriter 接口 的 WriteString 方法
  // 否则调用 Writer 的 Write 方法
    if sw, ok := w.(StringWriter); ok {
        return sw.WriteString(s)
    }
    return w.Write([]byte(s))
}

ReadAtLeast

ReadAtLeast:至少读取 min 个字节,即使用 Reader r 将至少 min 个字节读入字节数组 buf

入参

  • Reader r : 用于读数据
  • buf []byte : 保存读入的数据
  • min int: 至少读入 min 个字节

返回值

  • n: 成功读入的字节数
  • err: 产生的error
func ReadAtLeast(r Reader, buf []byte, min int) (n int, err error) {

    // 如果方法提供的字节数组 buf 长度小于 min,则不可能满足方法的定义,直接返回 ErrShortBuffer error
    if len(buf) < min {
        return 0, ErrShortBuffer
    }

    // n 记录当前读取的字节数,循环直至读取到的数据字节 n >= min
    for n < min && err == nil {
        var nn int
        nn, err = r.Read(buf[n:])
        n += nn
    }

    // 如果读取到的数据 n>=min,不管是否产生error,都返回nil,因为已经满足了至少读取min个字节
    if n >= min {
        err = nil
    } else if n > 0 && err == EOF {
        //如果读取到的数据少于 min 字节,但是读到了文件末尾产生了EOF,方法返回 ErrUnexpectedEOF error
        err = ErrUnexpectedEOF
    }

    // 其他case: 0 <= n < min ,直接返回当前 err

    return
}

几种 error

  1. 如果方法提供的字节数组 buf 长度小于 min,则不可能满足方法的定义,直接返回 ErrShortBuffer error。

  2. 如果读取到的数据大于0,少于 min 字节,但是读到了文件末尾产生了EOF,方法返回 ErrUnexpectedEOF error

  3. 如果读取到的数据大于等于 min 字节,但是产生了 error,因为已经满足的方法读取至少 min 个字节的需要,会丢弃这个 error,返回 nil

  4. 其他case:读取字节数为0,或者读取到的数据少于 min 字节,但是产生了其他非EOF error,方法返回相应的 error

ReadFull

ReadFull: 使用 Reader r 读取数据,将字节数组 buf 填充满

入参

  • Reader r
  • 字节数组buf

返回值

  • n:成功读取并写入字节数组buf的字节数
  • err:产生的error
func ReadFull(r Reader, buf []byte) (n int, err error) {
  // 该方法直接调用 ReadAtLeast(r, buf, len(buf)),相当于把 buf 字节数组读满。
    return ReadAtLeast(r, buf, len(buf))
}

参照 ReadAtLeast 的定义,可以有以下结论:

  • 如果返回的 err = EOF,那么一定没有读取任何数据,因为如果读取的数据 n>0,遇到 EOF 后,会返回 ErrUnexpectedEOF
  • 如果返回的 n=len(buf),那么 err一定为 nil

copyBuffer

copyBuffer 是一个私有方法,利用缓冲区 buffer,来完成 Reader 到 Writer 的数据复制。方法利用一个字节数组作为缓冲区, Reader 每次读取数据到字节数组中,然后 Writer 将字节数组中的数据写入文件,直至 Reader 读取数据结束,或者遇到 error。最终返回复制的字节数,以及复制过程中产生的 error。如果调用该方法时,传递的缓冲数组 buf =nil,会分配一个临时的字节数组。

func copyBuffer(dst Writer, src Reader, buf []byte) (written int64, err error) {

    // 如果 Reader src 实现了 WriteTo 接口,那么就可以调用 WriteTo 方法完成复制
    if wt, ok := src.(WriterTo); ok {
        return wt.WriteTo(dst)
    }

    // 如果 Writer 实现了 ReaderFrom 接口,那么就可以调用 ReadFrom 方法完成复制
    if rt, ok := dst.(ReaderFrom); ok {
        return rt.ReadFrom(src)
    }

    // 如果方法传递的缓冲区为nil,会分配一个缓冲区
    if buf == nil {

        // 默认为32 kb
        size := 32 * 1024

        // 如果是 src 是 LimitedReader,那么缓冲区大小要适应 LimitedReader
        if l, ok := src.(*LimitedReader); ok && int64(size) > l.N {
            // 至少为1
            if l.N < 1 {
                size = 1
            } else {
                // 最大不能超过 LimitedReader 的大小限制
                size = int(l.N)
            }
        }
        buf = make([]byte, size)
    }

    // 不断循环,从 Reader src 中读取数据到 buf 中,Writer dst 将读取到的数据写入
    for {
        nr, er := src.Read(buf)
        // 根据Reader 接口中 Read 方法的定义,先处理数据,再处理 error
        // nr>0,说明读取到了数据,那么 dst 就写入读取到的数据 buf[0:nr]
        if nr > 0 {
            nw, ew := dst.Write(buf[0:nr])

            // written 保存总计复制的字节大小
            if nw > 0 {
                written += int64(nw)
            }

            // 写入过程中遇到了err,结束复制
            if ew != nil {
                err = ew
                break
            }

            // 写入的字节数,与读取到的字节数不一致,结束复制,返回 ErrShortWrite
            if nr != nw {
                err = ErrShortWrite
                break
            }
        }

        // 如果读取过程中遇到了 er != nil,如果 er != EOF,最终就返回该 error ;
        // 如果 er = EOF,且逻辑走到这里,说明上面的写入没产生error,那么整个文件就复制完毕了,err 字段就没有赋值,最终返回的 err = nil
        if er != nil {
            if er != EOF {
                err = er
            }
            break
        }
    }
    // 返回总计复制的字节数,以及产生的error
    return written, err
}

Copy

Copy 的作用,就是将 Reader src 的数据,复制到 Writer dst 中,然后返回复制的字节数以及遇到的 error。Copy 直接调用的 copyBuffer 方法,但是没有提供缓冲数组,copyBuffer 方法内部会使用临时数组。

根据上面 copyBuffer 的定义,如果成功完成复制,返回值中 err=nil,而不是 err=EOF,因为如果遇到 EOF,说明已经复制完成了,err应该为nil。具体逻辑可以看上面 copyBuffer 源码的分析

func Copy(dst Writer, src Reader) (written int64, err error) {
    return copyBuffer(dst, src, nil)
}

CopyBuffer

CopyBuffer 与 Copy 方法不同的是,CopyBuffer 可以传入一个字节数组,用作全局缓冲区;Copy 方法不提供缓冲区,而是使用copyBuffer的临时缓冲区

func CopyBuffer(dst Writer, src Reader, buf []byte) (written int64, err error) {
    // 如果传入的字节缓冲区不为nil,但是长度为0,直接panic
    if buf != nil && len(buf) == 0 {
        panic("empty buffer in io.CopyBuffer")
    }
    // 如果传入的字节数组 buf=nil,那么就使用 copyBuffer 生成一个临时缓冲区
    // 如果传入的字节数组 buf!=nil,len(buf)>0,就使用 buf 作为全局缓冲区
    return copyBuffer(dst, src, buf)
}

CopyN

CopyN 的作用与 Copy 类似,都是用于从 Reader 复制数据到 Writer,但是 CopyN 限制了复制的字节数为 n,方法返回最终复制完成的长度 written 和产生的error err。

func CopyN(dst Writer, src Reader, n int64) (written int64, err error) {

    // 调用 Copy 方法复制数据,不过将Reader src 封装成了最多读取 n 个字节的 LimitReader,
    // 该LimitReader 最多读取n个字节,那么复制最多也就能完成 n 个字节
    written, err = Copy(dst, LimitReader(src, n))

    // written == n,说明成功复制,err=nil
    if written == n {
        return n, nil
    }

    //  err == nil,说明上面调用Copy方法完成了复制;
    //  written < n,说明 LimitReader 没有读取完 n 个字节就遇到了 EOF,方法没有完成复制n个字节的任务,返回 err = EOF
    if written < n && err == nil {
        // src stopped early; must have been EOF.
        err = EOF
    }

    // 其他错误,直接返回该错误即可

    return
}

总结

本篇文章对 io包 中的三个结构体以及方法进行了源码分析,主要内容如下:

  • 结构体
    • LimitedReader: 限制了读取的字节数
    • SectionReader: 限制了读取的范围
    • teeReader: 充当Reader 和 Writer 的桥梁,完成数据中转
  • 方法
    • WriteString: 写字符串
    • ReadAtLeast: 至少读取 min 个字节
    • ReadFull: 将传入的字节数组填充满
    • copyBuffer: 使用缓冲数组,完成 Reader 到 Writer 的数据复制
    • Copy: 不能提供全局缓冲数组,利用 copyBuffer 完成复制
    • CopyBuffer: 可以提供全局缓冲数组,利用 copyBuffer 完成复制
    • CopyN: 利用 LimitedReader,复制 n 个字节

看到这里,你是否也像我一样,惊艳于 go语言 接口设计的简洁和精妙呢?那就一起Keep Leaning 吧!

更多

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

微信公众号:漫漫Coding路

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

推荐阅读更多精彩内容