前言

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

结构体

LimitedReader

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

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

相关方法

  1. 返回 LimitedReader 实例
1
2
// 返回LimitReader 实例,r 作为底层 Reader,n 为限制读取的字节数大小
func LimitReader(r Reader, n int64) Reader { return &LimitedReader{r, n} }
  1. Read 方法
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// 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 的作用是限制了读取数据的范围,只能够读取原始数据中的某一个部分(或者说某一段)。定义如下:

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

相关方法

  1. 初始化 SectionReader 实例
1
2
3
4
// 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 方法:在指定的范围内读取数据
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// 读取数据到字节数组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 变量的值
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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 变量无关)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
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 方法:返回可读取数据的范围大小

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

teeReader

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

1
2
3
4
type teeReader struct {
   r Reader
   w Writer
}

相关方法

  1. 初始化方法:提供一个Reader 和 Writer 用于初始化
1
2
3
func TeeReader(r Reader, w Writer) Reader {
	return &teeReader{r, w}
}
  1. Read 方法:Reader r 将数据读取到字节数组 p 中,然后 Writer 将 p 中的数据写入到文件中,p 充当了数据的中转站。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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 用于将字符串写入文件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 入参: 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
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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
1
2
3
4
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,会分配一个临时的字节数组。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
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 源码的分析

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

CopyBuffer

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

1
2
3
4
5
6
7
8
9
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。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
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 吧!

更多

微信公众号:CodePlayer