go语言源码阅读:io标准库

3,136 阅读4分钟

前言

在go语言的io标准库中,CopyReadAll均可用于读写字节流,这里研究它们的源代码,并比较它们的性能差异。

这里使用的go语言版本为1.18。

io.ReadAll

ReadAll代码如下:

func ReadAll(r Reader) ([]byte, error) {
	b := make([]byte, 0, 512)
	for {
		if len(b) == cap(b) {
			// Add more capacity (let append pick how much).
			// 触发slice自动扩容(小于1024,扩容2倍,大于1024,扩容1.25倍)
			// 触发扩容后将添加的0移除
			// 如果要读取的内容很大,将会导致频繁扩容,
			b = append(b, 0)[:len(b)]
		}
		// 把slice未占用的传给Read
		// n是读取到的字节数,范围:0 <= n <= len(p)
		// 在for循环中,不断将r携带的s读取到b中
		n, err := r.Read(b[len(b):cap(b)])
		// 去掉未使用的空间
		// 查看Read的说明:调用者应该在处理err之前处理返回的n>0字节
		b = b[:len(b)+n]
		if err != nil {
			if err == EOF {
				err = nil
			}
			return b, err
		}
	}
}

详细解读见注释。从代码中可以看出,ReadAll读取的方式是通过r.Readr.s读取到b这个slice中,其中,r是一个interface,只要是实现了Reader接口的实例都可以作为参数,比如strings.Reader。在读取过程中,如果容量满了,则触发自动扩容。go语言slice的扩容规则是:长度小于1024,扩容2倍,大于1024,扩容1.25倍。扩容会带来较大的内存开销,所以当读取的内容可能较庞大时,应该是使用io.Copy

io.Copy

Copy代码:

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

实际调用的是copyBuffer

func copyBuffer(dst Writer, src Reader, buf []byte) (written int64, err error) {
	// If the reader has a WriteTo method, use it to do the copy.
	// Avoids an allocation and a copy.
	if wt, ok := src.(WriterTo); ok {
		return wt.WriteTo(dst)   // A
	}
	// Similarly, if the writer has a ReadFrom method, use it to do the copy.
	if rt, ok := dst.(ReaderFrom); ok {
		// ReadFrom是按每次512字节读取到buf里面
		return rt.ReadFrom(src)    // B
	}
	if buf == nil {   // C
		size := 32 * 1024  // 32KB
		// 如果传入的src是一个带有限制的Read(LimitedReader初始化的时候传入N【buf大小】)
		// 且N小于32KB
		if l, ok := src.(*LimitedReader); ok && int64(size) > l.N {
			if l.N < 1 {
				size = 1
			} else {
				size = int(l.N)
			}
		}
		buf = make([]byte, size)  // 创建一个size大小buf
	}
	for {
		nr, er := src.Read(buf)  // src.s copy到buf
		if nr > 0 {  // 还有内容
			nw, ew := dst.Write(buf[0:nr])  // buf又复制到dst.buf, buf相当于中间变量
			if nw < 0 || nr < nw {
				nw = 0
				if ew == nil {
					ew = errInvalidWrite
				}
			}
			written += int64(nw)  // 累计写入了多少
			if ew != nil {
				err = ew
				break
			}
			if nr != nw {
				err = ErrShortWrite
				break
			}
		}
		if er != nil { // 读取完毕或者有错误,结束
			if er != EOF {
				err = er
			}
			break
		}
	}
	return written, err
}

可以看出,根据src这个Reader所实现的接口的不同,分为三种情况(见注释中的A、B、C),以下分别说明。

A

如果src实现了WriterTo接口,则使用WriteTo进行读取:

func (r *Reader) WriteTo(w io.Writer) (n int64, err error) {
	r.prevRune = -1
	// 已经读取完毕
	if r.i >= int64(len(r.s)) {
		return 0, nil
	}
	// 剩下的部分
	s := r.s[r.i:]
	// 将s拷贝到w的buf 返回分配的长度
	m, err := io.WriteString(w, s)
	if m > len(s) {
		panic("strings.Reader.WriteTo: invalid WriteString count")
	}
	// 偏移量往前挪
	r.i += int64(m)
	n = int64(m)
	if m != len(s) && err == nil {
		err = io.ErrShortWrite
	}
	return
}

其主要功能是调用WriteString方法:

func WriteString(w Writer, s string) (n int, err error) {
	// 如果w实现了StringWriter接口
	if sw, ok := w.(StringWriter); ok {
		// 里面是分配好b.buf切片,然后将s拷贝进去
		return sw.WriteString(s)
	}
	return w.Write([]byte(s))
}

其中的sw.WriteString调用了tryGrowByReslicegrow,它们都优化了缓冲区(中间变量slice)的增长规则。sw.WriteString

func (b *Buffer) WriteString(s string) (n int, err error) {
	b.lastRead = opInvalid
	// 尝试通过重新切片的方式分配buf
	m, ok := b.tryGrowByReslice(len(s))
	if !ok {
		// 分配b.buf切片,返回分配的大小
		m = b.grow(len(s))
	}
	// 将字符串塞进b.buf切片里面
	return copy(b.buf[m:], s), nil
}

B

ReadFrom方法:

func (b *Buffer) ReadFrom(r io.Reader) (n int64, err error) {
	b.lastRead = opInvalid
	for {
		i := b.grow(MinRead)  // 按MinRead大小分配缓冲区(MinRead是个常量,值为512)
		b.buf = b.buf[:i]
		m, e := r.Read(b.buf[i:cap(b.buf)])
		if m < 0 {
			panic(errNegativeRead)
		}

		b.buf = b.buf[:i+m]
		n += int64(m)
		if e == io.EOF {
			return n, nil // e is EOF, so return nil explicitly
		}
		if e != nil {
			return n, e
		}
	}
}

C

C部分的逻辑是:先确定好缓冲区buf的大小,其大小可能是:1、32 * 1024;2、如果传入的src是一个*LimitedReader,则buf的大小由LimitedReaderN决定,N是在LimitedReader初始化的时候传入的。另外,如果N<1,则buf的值为1。

确定了buf的大小之后,程序开始以下循环:从src.s中拷贝数据到buf,又从buf复制数据到dst.buf,如此循环反复,直到读写出错或者已经读写完毕,程序返回写入的大小。

io.ReadAll 与 io.Copy 比较

查阅资料,很多资料说是对于读取大文件,io.ReadAll 比 io.Copy占用更多内存,因为频繁append需要重新分配内存,但实际测试了下,两者差别并不大。

测试程序放在:github.com/HubQin/gola… 分别用这两者下载一个3M大小的文件。

运行测试:切换到example2目录,运行,

go test -bench .

测试结果如下:

goos: windows
goarch: amd64
pkg: golang-source-code-reading/lib_io/example2
cpu: 12th Gen Intel(R) Core(TM) i5-12600K
BenchmarkDownFileWithReadAll-16                5         230262240 ns/op           55598 B/op         62 allocs/op
BenchmarkDownFileWithCopy-16                   5         320818280 ns/op           90499 B/op         69 allocs/op