一周一package之io包

175 阅读5分钟

缘起

记得好几年前到过介绍 python 包的网站,作者也是每周更新一个 package, 介绍一个包的用法。我这里也是按照这样的思想,每周会更新一个包的用户,这里不仅有基本的用法,也有源码的分析,也有该包在 NSQ 中的使用方式的分析。

记录总结为第一目的。业务逻辑代码写多了之后你就会有种自己能力在下降的感觉,这种需要静下来回归到经典中去,回归到经典的源码中去。只有在经典的官方 package 中你才能看到语言的精髓。

io package

基础 io 操作

还记得学习 C++ 的时候书中专门有一章来讲解 C++ 中 io 中每个类的继承关系,所有的语言语言中 io 包应该是最能体现语言抽象能力的 package。基于 linux 中 “一切皆文件” 的思想,一切的操作你都会涉及到 “” 和 “”。

  1. 在网络中你要从 socket 总读写数据

  2. 在文件操作中你要读写数据

  3. 本地 buffer 操作你也要读写数据

  4. 编解码操作

  5. 字符串操作

    golang 中是使用 interface 来表达抽象的,在 io package 中你会看到很多的 interface 定义,其中最关键的就是下面三个:

type Reader interface {
	Read(p []byte) (n int, err error)
}
type Writer interface {
	Write(p []byte) (n int, err error)
}
type Closer interface {
	Close() error
}   

这三个 interface 又会延伸出其他 interface:

  1. ReadWriter
  2. ReadCloser
  3. WriteCloser
  4. ReadWriteCloser

还有 seek 方式读写的 **Seeker, **以及从哪里读的 **ReaderFrom 和读到哪里的 WriterTo **等等,里面有超多的 interface 可以参考 godoc.org/io

网络数据的读取

// 一个很简单的 http handler
func (h *countHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	h.mu.Lock()
	defer h.mu.Unlock()
	h.n++
    // 这里 w 就实现了 Writer interface
	fmt.Fprintf(w, "count is %d\n", h.n)
}

本地buffer 操作

// 读取数据到本地buffer中
w := bufio.NewWriter(os.Stdout)
fmt.Fprint(w, "Hello, ")
fmt.Fprint(w, "world!")
w.Flush() // Don't forget to flush!

编解码操作

// 这里原型为
// func NewEncoder(enc *Encoding, w io.Writer) io.WriteCloser
// 这里传入的w参数需要实现 Writer interface
encoder := base64.NewEncoder(base64.StdEncoding, os.Stdout)
encoder.Write(input) // 这里最终会调用 w.Write([]byte) 方法

字符串操作

以 strings.NewReader 为例子,它实现了  io.Reader, io.ReaderAt, io.Seeker, io.WriterTo, io.ByteScanner, io.RuneScanner 等 interface, 实现了从字符串中的快速高效的读写:

r1 := strings.NewReader("first reader\n")
buf := make([]byte, 8)
// 这里 r1 实现了 Reader interface
if _, err := io.CopyBuffer(os.Stdout, r1, buf); err != nil {
    log.Fatal(err)
}

## pipe 以及 ioutil

ioutil 这个包从名字就很好理解,其中常用的就:

func ReadAll(r io.Reader) ([]byte, error)

func ReadFile(filename string) ([]byte, error)

func WriteFile(filename string, data []byte, perm os.FileMode) error

这三个函数。 pipe就很有意思了, pipe 的两端分别是一个reader 和 writer, 你可以理解为一个生产者和消费者。不过他们底层共享一个数据结构pipe。

func Pipe() (*PipeReader, *PipeWriter) {
		p := &pipe{
			wrCh: make(chan []byte),
			rdCh: make(chan int),
			done: make(chan struct{}),
		}
		return &PipeReader{p}, &PipeWriter{p}
	}

在读写的过程中, 读写的交流主要是靠 wrCh 和 **rdCh, **你可以看到他们之间并没有通过共享一个buffer 然后各自加锁来实现共享的, 而是通过channel来实现的共享。

Do not communicate by sharing memory; instead, share memory by communicating.

感觉 pipe 的用法就是 对上面这句 “设计哲学”最好的体现。 下一小节源码共享中会更多的分析 Pipe。

源码分析

源码分析这里选择两个函数一个是io.copyBuffer 函数,一个是pipe 操作。

io.copyBuffer

// 从 src 拷贝所有读到的数据到dst
// 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)
	}
   // 如果buf 没有指定,申请一个临时的 buf // 用于循环从src中拷贝数据到dst
	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 {
				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
}

pipe 操作

func (p *pipe) Write(b []byte) (n int, err error) {
	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: // 往channel 中写数据
			nw := <-p.rdCh // 等等读端 读走数据
			b = b[nw:]
			n += nw
		case <-p.done: // 安全关闭读写
			return n, p.writeCloseError()
		}
	}
	return n, nil
}
func (p *pipe) Read(b []byte) (n int, err error) {
	select {
	case <-p.done:
		return 0, p.readCloseError()
	default:
	}

	select {
	case bw := <-p.wrCh: // 收到数据
		nr := copy(b, bw) // 读取
		p.rdCh <- nr // 通知写侧, 我读走了多少数据
		return nr, nil
	case <-p.done: // 正常关闭处理
		return 0, p.readCloseError()
	}
}

从上面可以看出读写两边速度不一致是完全可以接受的,多个写都串行执行的。

在 nsq 中的使用

之前系统的看过几遍 nsq 中的代码,而且github 上 start 也有17.4k, 相信这里你也能看到golang 各种使用技巧。

  1. ./nsqd/protocol_v2.go 中处理从客户端收到的请求包
// 处理 客户端发送的Pub请求
func (p *protocolV2) PUB(client *clientV2, params [][]byte) ([]byte, error) {
  	messageBody := make([]byte, bodyLen)
    // 读取数据包
	_, err = io.ReadFull(client.Reader, messageBody) 
}
  1. nsqd/message.go 中将消息写入到buffer中
func (m *Message) WriteTo(w io.Writer) (int64, error) {
		n, err := w.Write(buf[:])
}
func writeMessageToBackend(buf *bytes.Buffer, msg *Message, bq BackendQueue) error {
		buf.Reset()
		_, err := msg.WriteTo(buf)
	}
  1. diskqueue.go 将消息写道磁盘上
// writeOne performs a low level filesystem write for a single []byte
// while advancing write positions and rolling files, if necessary
func (d *diskQueue) writeOne(data []byte) error {
	var err error
	d.writeBuf.Reset()
	err = binary.Write(&d.writeBuf, binary.BigEndian, dataLen)

}

io 操作 实在是太多了, 这里就不一一列举了。