缘起
记得好几年前到过介绍 python 包的网站,作者也是每周更新一个 package, 介绍一个包的用法。我这里也是按照这样的思想,每周会更新一个包的用户,这里不仅有基本的用法,也有源码的分析,也有该包在 NSQ 中的使用方式的分析。
记录总结为第一目的。业务逻辑代码写多了之后你就会有种自己能力在下降的感觉,这种需要静下来回归到经典中去,回归到经典的源码中去。只有在经典的官方 package 中你才能看到语言的精髓。
io package
基础 io 操作
还记得学习 C++ 的时候书中专门有一章来讲解 C++ 中 io 中每个类的继承关系,所有的语言语言中 io 包应该是最能体现语言抽象能力的 package。基于 linux 中 “一切皆文件” 的思想,一切的操作你都会涉及到 “读” 和 “写”。
-
在网络中你要从 socket 总读写数据
-
在文件操作中你要读写数据
-
本地 buffer 操作你也要读写数据
-
编解码操作
-
字符串操作
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:
- ReadWriter
- ReadCloser
- WriteCloser
- 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 各种使用技巧。
- ./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)
}
- 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)
}
- 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 操作 实在是太多了, 这里就不一一列举了。