刚开始学习 Go 的文件操作时,最容易产生一个疑问:
为什么 Go 里面读取文件要用
os,又出现了io,还有bufio?
刚开始看这些包的时候,会感觉它们功能很重复。
比如读取文件:
file.Read()也可以:
io.ReadAll(file)还可以:
bufio.NewReader(file)到底应该用哪个?
其实它们负责的事情完全不同。
一、先理解 IO 是什么
IO 的全称是:
Input / Output
也就是:
输入和输出。
简单理解:
程序需要和外部交换数据。
例如:
读取文件:
硬盘 | | 数据 ↓ Go程序写入文件:
Go程序 | | 数据 ↓ 硬盘这就是 IO。
二、Go 为什么设计 Reader 和 Writer?
假设没有接口。
读取文件:
file.Read()读取网络:
conn.Receive()读取字符串:
string.Get()每一种数据来源都有自己的方法。
那么代码会非常混乱。
Go 的设计:
不关心数据来自哪里。
只要求:
你能不能读取?
如果能:
实现:
io.Reader你能不能写入?
实现:
io.Writer于是:
文件:
os.File可以读。
字符串:
strings.Reader也可以读。
网络:
net.Conn也可以读。
它们虽然来源不同,但是使用方式一样:
Read()三、第一次读取文件
最简单:
package main import ( "fmt" "os" ) func main(){ file,err:=os.Open("hello.txt") if err!=nil{ return } defer file.Close() buf:=make([]byte,10) n,err:=file.Read(buf) fmt.Println(string(buf[:n])) }这个过程:
打开文件 ↓ 创建存储空间 ↓ Read读取 ↓ 打印数据但是这里有一个问题:
如果文件:
10GB怎么办?
你不能:
make([]byte,10GB)全部放内存。
所以出现:
四、流式读取
Go 不喜欢:
“一次性拿完”。
更推荐:
“一点一点处理”。
例如:
for{ n,err:=file.Read(buf) if n>0{ 处理数据 } if err==io.EOF{ break } }这就是流式 IO。
类似:
喝水。
不是一次喝完一桶。
而是一口一口喝。
五、bufio 为什么出现?
如果每次:
Read()都会访问磁盘。
比如:
读取一万个字符。
可能:
读取1个字符 访问磁盘 读取1个字符 访问磁盘 ...效率很低。
所以:
bufio 增加缓存。
变成:
文件 ↓ bufio缓存 ↓ 程序一次读取一批数据。
六、文件复制为什么用 io.Copy?
以前:
我们可能这样:
for{ 读取 写入 读取 写入 }但是 Go 已经帮我们封装:
io.Copy(dst,src)它内部:
Reader ↓ 缓冲 ↓ Writer自动完成。
所以:
文件复制:
io.Copy(target,source)就是最标准写法。
七、filepath 解决什么问题?
很多初学者会这样:
path := "data/"+nameLinux 可以。
Windows:
可能出现问题。
所以:
filepath.Join()帮你处理不同系统路径。
总结
学习 Go IO 最重要的不是记:
os.OpenFile bufio.Scanner filepath.Ext而是理解:
数据从哪里来? ↓ Reader ↓ 程序处理 ↓ Writer ↓ 数据到哪里去?文件:
os.File网络:
net.Conn内存:
bytes.Buffer最后都会统一成:
io.Reader io.Writer