Go基本库阅读:io库

2022-06-17 13:07:00 浏览数 (1)

io相关库

阅读标准库是我大一学haskell时养成的习惯,通过阅读标准库能够更好地掌握语言设计者认为这个语言应该如何使用,从而少走一些弯路。

io相关的操作我一直没怎么弄明白,特别是后面看到HTTP框架里面使用ioutil读取的时候。虽然C 的底子告诉我这样可以,但是我还是觉得应该一探究竟。

Golang的IO库那么多,我该怎么选?这篇文章比较概要地介绍了go中涉及到io的库:io库、os库、ioutil库、bufio库、bytes库、string库,其实net部分也有涉及,我看看之后能不能写到这个地方

可以进一步阅读的地方或资料:

  • bytes的buffer,很基本的操作,常见但是涉及到很多inline相关的优化
  • 1.1 io — 基本的 IO 接口
    • 这一章对于io库的叙述比较详尽,可以参考

io的常见用途

我个人学习东西的习惯是比较习惯从应用出发,自己什么时候用到这个东西,这个东西会在哪里被用到,怎么被用到。

说起IO,一般就是以下几种情况

  • 标准输入输出:fmt.Printf,fmt.Scanf
  • 读取文件
代码语言:javascript复制
package main

import (
	"bufio"
	"fmt"
	"io"
	"os"
)

func main() {
	inputFile, inputError := os.Open("input.dat")
	if inputError != nil {
		fmt.Printf("An error occurred on opening the inputfilen"  
			"Does the file exist?n"  
			"Have you got acces to it?n")
		return // exit the function on error
	}
	defer inputFile.Close()

	inputReader := bufio.NewReader(inputFile)
	for {
		inputString, readerError := inputReader.ReadString('n')
		if readerError == io.EOF {
			return
		}
		fmt.Printf("The input was: %s", inputString)
	}
}
  • 读取网络中body的信息(这个也是我做标准库阅读的初衷)
代码语言:javascript复制
//import package
import "io/ioutil"

//code snippet
    bodyBytes, err := ioutil.ReadAll(resp.Body)
    
 //---------- optioninal ---------------------
    //handling Errors
    if err != nil {
        log.Fatal(err)
    }
    
    //print result
    bodyString := string(bodyBytes)
    log.Info(bodyString)

io库

万变不离其宗,io库就是所有IO读写的核心。

IO中的核心概念是读和写,以此衍生出的概念是EOF(终止)、Seek(游走)、流或管道。基本所有的操作都是为了实现这些概念而做出来的。

基本结构体

最基本的结构体自然是Reader和Writer,基本方法为Read(bytes)和Write(bytes)。下面会详细介绍,但是与一般的想法不太一样,Read是将外部的数据写入到bytes数组中,而Write是将bytes中的数据写入到外部数据源。因此,应该将bytes理解成缓冲区,而数据源则为Reader或Writer自己带的。

与之相对的,WriteTo(Writer)和ReadFrom(Reader)则是相反的,ReadFrom会一直从Reader中读取数据,而WriteTo则会一直向Writer写入数据。

Read相关

Read代表了对数据的读取等操作

Reader

其中,Reader实现了Read方法:

代码语言:javascript复制
/*
方法从数组p中读取最多len(p)个bytes,返回实际读取的bytes数量n和返回的错误err。Reader不会阻塞去进行等待,如果数据没有准备好(实际数据与len(p)不匹配),它会直接返回
实现细节:
1. Read方法读取到最后一个bytes是返回n,EOF/nil,之后再调用则应该返回0,EOF
2. 不鼓励在读取到0个bytes后返回nil,除非len(p)==0。一般情况下0和nil不能同时返回,而且它绝不能暗示EOF。
3. 该方法的实现绝对不能保留数组p
调用者:
* 调用者应该先处理读到的bytes后,再考虑err的问题,从而将正常的错误与EOF同等对待。
*/
type Reader interface {
	Read(p []byte) (n int, err error)
}

Read方法的实现例子:

代码语言:javascript复制
// net包中conn有对应的实现,conn的Read依赖于netfd的Read,而后者依赖于bufferedPipe的Read
// 位置:src/net/net_fake.go:186
func (p *bufferedPipe) Read(b []byte) (int, error) {
	p.mu.Lock()
	defer p.mu.Unlock()
  // 加锁后直接循环
  // 与官方实现说的有点不太一样,Read实际上是可以实现等待的,只是需要自行设置DDL
	for { 
		if p.closed && len(p.buf) == 0 {
			return 0, io.EOF // 管道关闭,或者缓冲为0时返回。
		}
		if !p.rDeadline.IsZero() { // 如果存在DDL,则执行对应的逻辑
			d := time.Until(p.rDeadline)
			if d <= 0 { // DDL到了,则返回对应的信号
				return 0, syscall.EAGAIN
			}
			time.AfterFunc(d, p.rCond.Broadcast) // 到时间后,BroadCast会唤醒p.rCond中的所有协程
		}
		if len(p.buf) > 0 { // 缓存区中有数据,则直接弹出
			break
		}
		p.rCond.Wait()
	}

	n := copy(b, p.buf) // 直接将一个slice的元素拷贝给另外一个slice,n表示实际复制成功的slice元素个数
	p.buf = p.buf[n:] // 进行截断,避免重复
	p.wCond.Broadcast() // 唤醒所有的wCond
	return n, nil
}

PS:如果要进一步了解IO的细节,可以阅读go_epoll

IO库的Reader与底层关系太近,bytes中的可能要稍微好一些

代码语言:javascript复制
// Read的位置为src/bytes/reader.go:39
// A Reader implements the io.Reader, io.ReaderAt, io.WriterTo, io.Seeker,
// io.ByteScanner, and io.RuneScanner interfaces by reading from
// a byte slice.
// Unlike a Buffer, a Reader is read-only and supports seeking.
// The zero value for Reader operates like a Reader of an empty slice.
type Reader struct {
	s        []byte
	i        int64 // current reading index
	prevRune int   // index of previous rune; or < 0
}

// Read implements the io.Reader interface.
func (r *Reader) Read(b []byte) (n int, err error) {
	if r.i >= int64(len(r.s)) { // 如果当前的index大于所读取的数据数组s,则EOF
		return 0, io.EOF // 这里与之前的文档一致,0和nil不同时出现,0应该跟着EOF,除非len(p)==0
	}
	r.prevRune = -1 // 这里应该是单纯的用不上
// 简单的使用copy
	n = copy(b, r.s[r.i:])
	r.i  = int64(n)
	return
}
其他Reader相关的基本操作

ReadAt和ReadFrom

代码语言:javascript复制
/*
与Read相比,ReadAt会在输入源的index==off的位置开始读取
如果n<len(p),ReadAt必须返回错误来告知为什么没有读取完整。如果数据源可用但长度非len(p),ReadAt会一直Block到出结果或者err
PS: 从前面的网络包实现中可以发现,实际实现中并没有严格遵循这一点
与Read不同的另外一点在于,ReadAt的Client要能够同时对同一个输入源进行操作。与之相对,尽管没有明说,但是Read的网络包实现中是加了锁的
*/
type ReaderAt interface {
	ReadAt(p []byte, off int64) (n int, err error)
}

// src/bytes/reader.go中同样实现了ReadAt的方法
// ReadAt implements the io.ReaderAt interface.(原注释,我对此的理解是因为要支持并发操作,因此如果对状态进行修改将会产生问题)
func (r *Reader) ReadAt(b []byte, off int64) (n int, err error) {
	// cannot modify state - see io.ReaderAt
// legal check
	if off < 0 {
		return 0, errors.New("bytes.Reader.ReadAt: negative offset")
	}
	if off >= int64(len(r.s)) {
		return 0, io.EOF
	}

	n = copy(b, r.s[off:])
	if n < len(b) {
		err = io.EOF // 比Read多的地方,如果n<len(b)时会额外增加这个错误
	}
	return
}

/*
ReadFrom的目标是从Reader中一直读取数据直到读到其他错误或者EOF
*/
type ReaderFrom interface {
	ReadFrom(r Reader) (n int64, err error)
}
// src/bytes/buffer.go:197中实现了这一方法,会一直从某个reader中读取数据直到读完为止
// ReadFrom reads data from r until EOF and appends it to the buffer, growing
// the buffer as needed. The return value n is the number of bytes read. Any
// error except io.EOF encountered during the read is also returned. If the
// buffer becomes too large, ReadFrom will panic with ErrTooLarge.
func (b *Buffer) ReadFrom(r io.Reader) (n int64, err error) {
	b.lastRead = opInvalid
	for {
		i := b.grow(MinRead) // MinRead=512,是单个Read调用的最小的slice size。而grow则会扩展slice从而保证额外的MinRead大小的数据能够被写入
		b.buf = b.buf[:i] // 确保len正确,从切片中新建切片不会改变cap
		m, e := r.Read(b.buf[i:cap(b.buf)])
		if m < 0 {
			panic(errNegativeRead)
		}
		// 读到了m个数据。这里体现了一个特点,即Read的err的处理逻辑是先处理数据再处理错误
		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
		}
	}
}
延伸Reader

主要是SectionReader

代码语言:javascript复制
/*
SectionReawder可以指定读取某一个范围的数据,该范围为从off之后n个点的数据,在读完n个点之后返回EOF
*/
type SectionReader struct {
	r     ReaderAt
	base  int64
	off   int64
	limit int64
}

func NewSectionReader(r ReaderAt, off int64, n int64) *SectionReader

官方给的例子:

代码语言:javascript复制
package main

import (
	"fmt"
	"io"
	"log"
	"strings"
)

func main() {
	r := strings.NewReader("some io.Reader stream to be readn")
	s := io.NewSectionReader(r, 5, 17)

	buf := make([]byte, 9)
	if _, err := s.Read(buf); err != nil {
		log.Fatal(err)
	}

	fmt.Printf("%sn", buf)

}

Writer

write

Writer则需要实现Write方法

代码语言:javascript复制
/*
与Read方法相对,write方法会从数组p中读取至多len(p)的数据写入到基本数据流中。
其中实现并不一定要包含p,比如Discard就没有使用到p数组。
问题:基本数据流该怎么调用?它是结构体的成员之一吗
*/
type Writer interface {
	Write(p []byte) (n int, err error)
}
// src/bytes/buffer.go
// Write appends the contents of p to the buffer, growing the buffer as
// needed. The return value n is the length of p; err is always nil. If the
// buffer becomes too large, Write will panic with ErrTooLarge.
func (b *Buffer) Write(p []byte) (n int, err error) {
	b.lastRead = opInvalid
	m, ok := b.tryGrowByReslice(len(p))
	if !ok {
		m = b.grow(len(p))
	}
// 直接通过拷贝来实现写入
	return copy(b.buf[m:], p), nil
}
其他Writer相关的基本操作

主要是WriteAt和WriteTo

代码语言:javascript复制
// src/bytes/buffer.go:249
// WriteTo writes data to w until the buffer is drained or an error occurs.
// The return value n is the number of bytes written; it always fits into an
// int, but it is int64 to match the io.WriterTo interface. Any error
// encountered during the write is also returned.
func (b *Buffer) WriteTo(w io.Writer) (n int64, err error) {
// 函数将b.buf的数据写入到w中
	b.lastRead = opInvalid
	if nBytes := b.Len(); nBytes > 0 {
		m, e := w.Write(b.buf[b.off:]) // 尝试向w中写入
		if m > nBytes { // 写入异常
			panic("bytes.Buffer.WriteTo: invalid Write count")
		}
		b.off  = m
		n = int64(m)
		if e != nil {
			return n, e
		}
		// all bytes should have been written, by definition of
		// Write method in io.Writer
		if m != nBytes {
			return n, io.ErrShortWrite
		}
	}
	// Buffer is now empty; reset.
	b.Reset()
// 由于采用了if,所以这里使用的是直接对n进行赋值的方法,而不是如ReadFrom一样的循环累加
	return n, nil
}

// src/os/file.go:200
// WriteAt writes len(b) bytes to the File starting at byte offset off.
// It returns the number of bytes written and an error, if any.
// WriteAt returns a non-nil error when n != len(b).
//
// If file was opened with the O_APPEND flag, WriteAt returns an error.
func (f *File) WriteAt(b []byte, off int64) (n int, err error) {
	if err := f.checkValid("write"); err != nil {
		return 0, err
	}
	if f.appendMode {
		return 0, errWriteAtInAppendMode
	}

	if off < 0 {
		return 0, &PathError{Op: "writeat", Path: f.name, Err: errors.New("negative offset")}
	}

	for len(b) > 0 {
		m, e := f.pwrite(b, off)
		if e != nil {
			err = f.wrapErr("write", e)
			break
		}
		n  = m
		b = b[m:]
		off  = int64(m)
	}
	return
}

Seeker

代码语言:javascript复制
/*
Seek方法将下一个Read或者Write的index设置为offset,而这个offset的解释则根据whence进行。
SeekStart与开始相关,SeekCurrent与当前相关,SeekEnd与结尾相关
Seek返回处理后得到的新的offset以及对应的错误
*/
type Seeker interface {
	Seek(offset int64, whence int) (int64, error)
}
// 实现示范,src/io/io.go
func (s *SectionReader) Seek(offset int64, whence int) (int64, error) {
	switch whence {
	default:
		return 0, errWhence
	case SeekStart:
		offset  = s.base
	case SeekCurrent:
		offset  = s.off
	case SeekEnd:
		offset  = s.limit
	}
	if offset < s.base {
		return 0, errOffset
	}
	s.off = offset
	return offset - s.base, nil
}

管道以及常见函数

没什么特别的地方,不赘述。详见:1.1 io — 基本的 IO 接口

其中,管道的实现代码:

代码语言:javascript复制
// src/io/pipe.go:199
func Pipe() (*PipeReader, *PipeWriter) {
	p := &pipe{
		wrCh: make(chan []byte), // write channel
		rdCh: make(chan int), // read channel
		done: make(chan struct{}), // done channel
	}
	return &PipeReader{p}, &PipeWriter{p}
}

func (p *pipe) read(b []byte) (n int, err error) {
	// 从管道中读取到b
	select {
	case <-p.done: // 如果完成,则关闭
		return 0, p.readCloseError()
	default:
	}

	select {
	case bw := <-p.wrCh: // 从writechannel中读数据
		nr := copy(b, bw)
		p.rdCh <- nr
		return nr, nil
	case <-p.done:
		return 0, p.readCloseError()
	}
}

func (p *pipe) write(b []byte) (n int, err error) {
  // 从b中写入到管道
	select {
	case <-p.done:
		return 0, p.writeCloseError()
	default:
		p.wrMu.Lock()
		defer p.wrMu.Unlock()
	}

	for once := true; once || len(b) > 0; once = false {
		select {
		case p.wrCh <- b:
			nw := <-p.rdCh
			b = b[nw:]
			n  = nw
		case <-p.done:
			return n, p.writeCloseError()
		}
	}
	return n, nil
}

Copy的实现代码:

代码语言:javascript复制
// Copy copies from src to dst until either EOF is reached
// on src or an error occurs. It returns the number of bytes
// copied and the first error encountered while copying, if any.
//
// A successful Copy returns err == nil, not err == EOF.
// Because Copy is defined to read from src until EOF, it does
// not treat an EOF from Read as an error to be reported.
//
// If src implements the WriterTo interface,
// the copy is implemented by calling src.WriteTo(dst).
// Otherwise, if dst implements the ReaderFrom interface,
// the copy is implemented by calling dst.ReadFrom(src).
func Copy(dst Writer, src Reader) (written int64, err error) {
	return copyBuffer(dst, src, nil)
}

// CopyBuffer is identical to Copy except that it stages through the
// provided buffer (if one is required) rather than allocating a
// temporary one. If buf is nil, one is allocated; otherwise if it has
// zero length, CopyBuffer panics.
//
// If either src implements WriterTo or dst implements ReaderFrom,
// buf will not be used to perform the copy.
func CopyBuffer(dst Writer, src Reader, buf []byte) (written int64, err error) {
	if buf != nil && len(buf) == 0 {
		panic("empty buffer in CopyBuffer")
	}
	return copyBuffer(dst, src, buf)
}

// copyBuffer is the actual implementation of Copy and CopyBuffer.
// if buf is nil, one is allocated.
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)
	}
	// Similarly, if the writer has a ReadFrom method, use it to do the copy.
	if rt, ok := dst.(ReaderFrom); ok {
		return rt.ReadFrom(src)
	}
	if buf == nil {
		size := 32 * 1024
		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)
	}
	for {
		nr, er := src.Read(buf)
		if nr > 0 {
			nw, ew := dst.Write(buf[0:nr])
			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
}

ioutil库

ioutil-gitbook

而在Go1.16中,对应的函数被io或os库的函数实现,不应该调用ioutil

比较重要的几个函数

RealAll

golang中的ioutil.ReadAll vs io.Copy

0 人点赞