Skip to content

Go 学习笔记 02 - 找准 io 之道

Published: at 17:21

嘛,好久一年半不写 Go 相关的文章的,这期我们来看一看 Goio 库。

ioGo 中非常重要,但同时也非常简单的部分。它抽象出了最基本的 ReaderWriter,并实现了 MultiWriterPipe。代码很简单,很简单,但是却是一切的基础。正是因为它非常简单,这篇文章才能带上源码讲(

godoc 固然方便,但它也有它的缺点,就是取消了文件之间的障碍,反而增加了整理理解的难度,并且打乱了阅读的思路。因此也只能作为一个字典式的工具存在,难以代替源码阅读本身的意义。

(或许 Go 的这几篇文章也能成为一个系列呢,谁知道呢(找准 golang 之道

不可能了,多久没写 Go 了,这篇估计就是最后一篇了,而且还是存货(

ToC

ReaderWriter

饼一口一口吃,我们先来看最基本的两个 interfaceReaderWriter

ReaderWriter相对数据的概念,因此 Read 就意味着从数据中读,而 Write 就是向数据中写。因此在下面我们会发现,Reader 需要提供一个 []byte 作为输入,它就是用来承载从数据中读出的内容的,执行完成后数组中内容发生改变;而 Write 则恰恰完全相反,它虽然也许要提供一个 []byte,但这个数组是用来写入的,执行完成后数组中的内容不会改变

在看详细的内容之前,我们先来看一看整个文件开头的注释。行号代表的是在 io.go 中的位置,下同。

1
// Copyright 2009 The Go Authors. All rights reserved.
2
// Use of this source code is governed by a BSD-style
3
// license that can be found in the LICENSE file.
4
5
// Package io provides basic interfaces to I/O primitives.
6
// Its primary job is to wrap existing implementations of such primitives,
7
// such as those in package os, into shared public interfaces that
8
// abstract the functionality, plus some other related primitives.
9
//
10
// Because these interfaces and primitives wrap lower-level operations with
11
// various implementations, unless otherwise informed clients should not
12
// assume they are safe for parallel execution.

这里值得我们注意的是最后一段:io 对并行安全性不作保证。这很符合常识,不是吗(

下面我们来看相对详细的内容。

Reader

1
type Reader interface {
2
Read(p []byte) (n int, err error)
3
}

可以看到,这个接口只有一个 Read 函数,参数是上面我们提到的 []byte,返回值是 nerrerr 很好理解,那 n 又是什么呢?

这时候我们回去看上面的注释。注释就解释得很明白:

1
// Reader 是包装了 Read 方法的接口
2
//
3
// Read 方法读取最多 len(p) 字节并保存到 p
4
// 它返回它读入的字节数 n(0 <= n <= len(p)) 和发生的任何错误
5
// 即使它的返回值 n < len(p) 它也有可能因在调用过程中将 p 作为暂存空间而使用整个 p
6
// 如果某些数据可用但长度并非 p 字节
7
// Read 方法传统上返回可用的字节数而不等待更多
8
//
9
// 当 Read 方法遇到错误或在读取 n > 0 个字符后出现 EOF
10
// 它返回已读的字节数 n
11
// 它可能会在一次调用中返回 非 nil 的 error
12
// 或在随后的调用中返回 0, err
13
// 这种情况的一个例子:当 Reader 读到输入流末端时
14
// 它会在这次 Read 时返回一个非 0 的字节数和一个 err == nil 或 err == EOF
15
// 下次 Read 则返回 0, EOF
16
//
17
//
18
// 调用时始终需要先处理返回的 n > 0 字节 然后再处理 err
19
// 这样做能够正确处理一些发生在已经读取了一些字节之后的 I/O 错误
20
// 以及被允许的 EOF 行为
21
//
22
//
23
// 实现不建议同时返回 n = 0 和 err = nil
24
// 除非 len(p) == 0
25
// 调用时需要将返回 0, nil 视为无事发生
26
// 特别是它不代表 EOF
27
//
28
// 实现不能保留 p

可以看到,注释中既有对使用者的说明,也有对实现者的实现建议。这里我们作为使用者,始终需要记住的一点就是:不要因为 err 就丢弃 n

这或许和我们平常使用的错误处理不同,这也是 Reader 的特点之一:无论是否出现错误,n 代表的内容是始终需要处理的。

Writer

1
type Writer interface {
2
Write(p []byte) (n int, err error)
3
}

同理,这里的 Writer 也需要遵守上面的一些规则。这里需要补充的是以下两条:

  1. n < len(p) 时,err 必须返回 非 nil
  2. Write 禁止p 作哪怕是暂时的任何修改

CloserSeeker

看完了最常用的两个,接下来来看看虽然没那么常用,但其实也很常用的两个接口:CloserSeeker

Closer

1
type Closer interface {
2
Close() error
3
}

Closer 是包含了 Close 方法的接口,基本用途正如其名。这里需要注意的只有一点:Close 的多次调用是未定义行为,遇到时请具体问题具体分析(指查对应的文档

Seeker

1
type Seeker interface {
2
Seek(offset int64, whence int) (int64, error)
3
}

Seeker 相对来说稍微复杂那么一点点,但也很简单。offset 对应的是偏移,而 whence 代表的是三种可能:SeekStartSeekCurrentSeekEnd,代表 offset 是相对开头,当前位置还是结尾的。

<heimu>retime</heimu>

排列组合

在上四者都定义完之后,就是一大堆的排列组合了(准确说是组合),有:

ReadFromWriteTo

接下来的这一对接口就相对比较靠近应用层了。ReadFrom 是从 Reader 中读取所有信息到自身,而 WriteTo 则是将自身所有信息写入 Writer。当实现了下面这两个接口时,io.Copy 会优先使用这两个接口。我们来看:

ReadFrom

1
type ReaderFrom interface {
2
ReadFrom(r Reader) (n int64, err error)
3
}

ReadFrom 函数会持续尝试 Read,直到 EOF 为止。这里需要注意的是:当 r.Read 返回 EOF 的错误时,错误应一并返回。

WriteTo

1
type WriterTo interface {
2
WriteTo(w Writer) (n int64, err error)
3
}

ReadFrom 类似,WriteTo 则是尽可能地向 Writer 中写入数据,返回 n 表示写入的字节数,err 表示错误。同理,当 w.Write 产生错误时,WriteTo 也要把这个错误返回。

结语

除了上文中描述的内容,io 中还包含了一些对常用类型的包装,如 StringWriterByte 系列的 ReaderWriterScanner,以及结合了 ReadWritePipe。使用者可以结合实际情况去实现对应的 interface,赋予 struct 更多的 I/O 能力。

嘛,就是这样(估计不会有下一篇了(除非真香