189 lines
6.4 KiB
Markdown
189 lines
6.4 KiB
Markdown
# Golang IO 相关操作
|
||
|
||
- io 包提供了基础的 io 操作,几乎所有的 io 操作都是基于 io.Reader 和 io.Writer 接口的。
|
||
- ~~ioutil 包提供了一些方便的 io 操作函数,但是在 go 1.16 中已经被废弃,放进了 io 包。~~
|
||
- bufio 实现了缓冲 io,可以提高 io 效率。
|
||
- bytes 包中提供了 Buffer 类型,可以用来做 io 操作。
|
||
|
||
## 核心接口
|
||
|
||
Go 中几乎所有的 io 操作都是围绕着 Reader/Writer/Seeker 这三个接口进行,这三个接口都是独立开来的,可以随机组合。
|
||
|
||
Reader 和 Writer 是 io 包中最重要的接口,它们定义了 io 操作的基本行为。像一些文件操作,网络操作等等,都是基于这两个接口的。
|
||
|
||
Seeker 接口定义了 Seek 方法,可以在 Reader/Writer 中定位到指定的位置,文件操作中常用。
|
||
|
||
还有一个 Close 接口,用来关闭 io,也是经常使用,有这个接口的时候,我们可以使用 defer 来关闭,避免忘记,而且一般情况下都需要注意关闭,否则会产生内存泄漏之类的问题。
|
||
|
||
有很多操作也是需要执行完 Close 之后才能生效,例如 gzip 压缩,如果不 Close,可能会导致压缩文件不完整。
|
||
|
||
```go
|
||
func TestGzip(t *testing.T) {
|
||
file := bytes.NewBuffer([]byte("hello world"))
|
||
buf := bytes.NewBuffer(nil)
|
||
w := gzip.NewWriter(buf)
|
||
_, _ = io.Copy(w, file)
|
||
t.Logf("不Close文件长度: %d\n", buf.Len())
|
||
w.Close()
|
||
t.Logf("压缩后文件长度: %d\n", buf.Len())
|
||
}
|
||
```
|
||
|
||
### 基础使用
|
||
|
||
别看这三个接口简单,他们可以玩出很多花出来。例如对 Reader/Writer 进行包装,实现读取写入大小统计,来做流量统计时这很有用;标准库中也有很多方法,值得可以学习。
|
||
|
||
```go
|
||
type WrapReader struct {
|
||
len int
|
||
r io.Reader
|
||
}
|
||
|
||
func (w *WrapReader) Read(p []byte) (int, error) {
|
||
n, err := w.r.Read(p)
|
||
w.len += n
|
||
return n, err
|
||
}
|
||
|
||
func TestReader(t *testing.T) {
|
||
file := bytes.NewBuffer([]byte("hello world"))
|
||
wr := WrapReader{len: 0, r: file}
|
||
_, _ = io.Copy(io.Discard, &wr)
|
||
t.Logf("文件长度: %d\n", wr.len)
|
||
}
|
||
```
|
||
|
||
## 常用函数
|
||
|
||
下面我们来看一些常用的 io 操作函数,更多的函数可以查看官方文档,我只是列出一些我常用到的。
|
||
|
||
- io.ReadAll 读取所有数据,返回一个 bytes
|
||
- io.MultiWriter/Reader 可以将多个 Reader/Writer 合并成一个
|
||
- io.LimitReader 限制 Reader 的读取长度,读取 n 个长度后返回 io.EOF
|
||
- io.TeeReader 读取数据的同时写入到另一个 Writer 中
|
||
- io.Pipe 创建同步内存管道
|
||
- io.Copy/CopyN 复制 Reader 到 Writer
|
||
- io.NopCloser 给一个 io.Reader 加上 Close 方法
|
||
- bytes.NewBuffer 创建一个 Buffer
|
||
- bufio.NewReader/Writer 创建一个缓冲 Reader/Writer
|
||
|
||
## 常见小坑
|
||
|
||
> 我暂时能想到的坑记录下来,欢迎补充
|
||
|
||
### io.EOF
|
||
|
||
io.EOF 是 io 包中定义的一个错误,表示读取到文件末尾。在读取文件时,当读取到文件末尾时,会返回 io.EOF 错误。
|
||
|
||
请注意,当读取到文件末尾时,返回的数据可能不是 nil,而是文件的最后一部分数据,需要注意处理。
|
||
|
||
### 大文件的操作
|
||
|
||
在处理大文件时,需要注意内存的使用,尽量使用 io.Reader 和 io.Writer 来处理文件,避免一次性读取整个文件到内存中。
|
||
|
||
### bufio.Writer.Flush
|
||
|
||
在使用 bufio.Writer 时,需要注意 Flush 方法,因为 bufio.Writer 是带缓冲的,数据并不是实时写入到 Writer 中的,需要调用 Flush 方法来刷新缓冲区。
|
||
|
||
### io.Pipe
|
||
|
||
io.Pipe 是一个同步的内存管道,请注意在使用时,Reader 与 Writer 必须在不同的 goroutine 中,且需要注意 Reader 与 Writer 的速度需要一致,否则会造成死锁或线程阻塞。
|
||
|
||
并且需要使用 Close 方法来关闭 Reader 或 Writer,否则会造成内存泄漏。
|
||
|
||
## 高级用例
|
||
|
||
下面介绍一些高级用例,你也可以从中学习到一些高级的 io 操作技巧。
|
||
|
||
### 计算上传文件的 hash
|
||
|
||
下面的例子是计算上传文件的 md5 和 sha1 的 hash 值,我们使用 io.TeeReader 将文件内容同时写入到 md5 和 sha1 的 hash 计算器中。
|
||
像我们在 http 上传文件时,可以在上传的同时计算文件的 hash 值,这样可以保证文件的完整性。同时也可以打开文件,再将文件的 Writer 放到 io.Copy 中,这样可以同时写入文件和计算 hash 值。
|
||
|
||
```go
|
||
func TestIOHash(t *testing.T) {
|
||
file := bytes.NewBuffer([]byte("hello world"))
|
||
md5 := md5.New()
|
||
sha1 := sha1.New()
|
||
r := io.TeeReader(file, io.MultiWriter(md5, sha1))
|
||
_, _ = io.Copy(io.Discard, r)
|
||
t.Logf("md5: %x\n", md5.Sum(nil))
|
||
t.Logf("sha1: %x\n", sha1.Sum(nil))
|
||
}
|
||
```
|
||
|
||
### 读取文件的部分内容
|
||
|
||
下面的例子是读取文件的部分内容,我们使用 io.LimitReader 限制读取的长度,读取 n 个长度后返回 io.EOF,这在解析文件头部信息时非常有用。
|
||
|
||
```go
|
||
func TestLimitReader(t *testing.T) {
|
||
file := bytes.NewBuffer([]byte("hello world"))
|
||
lr := io.LimitReader(file, 5)
|
||
data, _ := io.ReadAll(lr)
|
||
t.Logf("%s\n", data)
|
||
}
|
||
```
|
||
|
||
### 大文件按行读取
|
||
|
||
下面的例子是读取大文件的每一行,我们使用 bufio.Reader 来读取文件的每一行,这样可以避免一次性读取整个文件到内存中。
|
||
|
||
在读一些 io 比较慢的文件时(网络文件),也可以使用这种方式来读取文件。写文件也是类似的,可以使用 bufio.Writer 来写文件,提高磁盘 io 效率,避免频繁的 io 操作。
|
||
|
||
```go
|
||
func TestBufIo(t *testing.T) {
|
||
file := bytes.NewBufferString("hello world\ngolang")
|
||
buf := bufio.NewReader(file)
|
||
for {
|
||
line, err := buf.ReadString('\n')
|
||
if err == io.EOF {
|
||
t.Logf("%s", line)
|
||
break
|
||
}
|
||
t.Logf("%s", line)
|
||
}
|
||
}
|
||
```
|
||
|
||
### 使用 Pipe 将上传文件解密写入文件
|
||
|
||
有时候上传的文件是一个加密的,我们需要在上传的同时解密文件,然后同时写入到磁盘中,这时候可以使用 io.Pipe 来实现。
|
||
|
||
```go
|
||
|
||
// 凯撒加密
|
||
type decode struct {
|
||
r io.Reader
|
||
}
|
||
|
||
func (d *decode) Read(p []byte) (int, error) {
|
||
// 读取数据
|
||
n, err := d.r.Read(p)
|
||
if err != nil {
|
||
if err != io.EOF {
|
||
return n, err
|
||
}
|
||
}
|
||
// 解密
|
||
for i := 0; i < n; i++ {
|
||
p[i]++
|
||
}
|
||
return n, err
|
||
}
|
||
|
||
func TestDecodePipe(t *testing.T) {
|
||
body := bytes.NewBuffer([]byte("gdkkn\u001Fvnqkc"))
|
||
r, w := io.Pipe()
|
||
defer r.Close()
|
||
go func() {
|
||
defer w.Close()
|
||
// 读取加密数据解密
|
||
_, _ = io.Copy(w, &decode{r: body})
|
||
}()
|
||
// 读取解密数据,保存文件
|
||
data, _ := io.ReadAll(r)
|
||
t.Logf("%s\n", data)
|
||
}
|
||
```
|