os.File.Write
这是最传统的方式,直接跟磁盘文件交互。
func TestOsWriteFile(t *testing.T) {
ff, err := os.OpenFile("./file_os", os.O_CREATE|os.O_RDWR|os.O_APPEND, os.ModePerm)
if err != nil {
t.Fatal(err)
}
defer ff.Close()
_, err = ff.Write([]byte("Hello World\n"))
if err != nil {
t.Fatal(err)
}
}
我们可以选择os.Create创建文件,它实际上也是调用os.OpenFile
func Create(name string) (*File, error) {
return OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0666)
}
这里介绍几个常用的flag:
- O_CREATE:文件不存在时创建,如不加这个flag,当文件不存在时会报错;
- O_RDWR,O_RDONLY,O_WRONLY:读写权限;
- O_TRUNC:打开时删除原内容,从头写入;
- O_APPEND:追加写入;
os.WriteFile
这种方式和上一种没有区别,只是调用变简单了些。
func TestIOWriteFile(t *testing.T) {
// 每次写入会truncate
os.WriteFile("./file_ioutil", []byte("hello world\n"), os.ModePerm)
}
在1.16版本之前,这个函数放在ioutil中,它的实现很简单,就类似于上面我们自己写的代码:
func WriteFile(name string, data []byte, perm FileMode) error {
f, err := OpenFile(name, O_WRONLY|O_CREATE|O_TRUNC, perm)
if err != nil {
return err
}
_, err = f.Write(data)
if err1 := f.Close(); err1 != nil && err == nil {
err = err1
}
return err
}
bufio.Write
bufio 顾名思义,在io基础上加了buffer。好处是不用每次写入都直接落到磁盘,而是先缓存在内存中,当buffer写满后再一次性落盘。好处当然是减少了磁盘io,提高了性能。
func TestBufWriteFile(t *testing.T) {
ff, err := os.OpenFile("./file_bufio", os.O_CREATE|os.O_RDWR|os.O_APPEND, os.ModePerm)
if err != nil {
t.Fatal(err)
}
defer ff.Close()
bw := bufio.NewWriter(ff)
nn, err := bw.Write([]byte("hello world\n"))
if err != nil {
t.Fatal(err)
}
t.Log("write byte: ", nn)
bw.Flush()
}
我们简单看下bufio的内部实现,首先是创建
// NewWriter会创建一个默认大小(4KB)的缓冲区
func NewWriter(w io.Writer) *Writer {
return NewWriterSize(w, defaultBufSize)
}
func NewWriterSize(w io.Writer, size int) *Writer {
b, ok := w.(*Writer)
// 如果w已经是bufio.Writer,且它的缓冲区更大,就直接返回
if ok && len(b.buf) >= size {
return b
}
if size <= 0 {
size = defaultBufSize
}
// bufio.Writer 实际就是在io.Writer的基础上,加了个buf
return &Writer{
buf: make([]byte, size),
wr: w,
}
}
然后是Write函数
func (b *Writer) Write(p []byte) (nn int, err error) {
// 如果p的长度超出了缓冲区可用大小
for len(p) > b.Available() && b.err == nil {
var n int
if b.Buffered() == 0 {
// 如果缓冲区是空的,但p的大小比整个缓冲区都大,那就直接写到磁盘去,省的先拷贝到
// 缓冲区再写磁盘
n, b.err = b.wr.Write(p)
} else {
// buffer已有一些数据,这时候把p的一部分写到buffer
// 此时buffer已经满了,所以Flush到磁盘
n = copy(b.buf[b.n:], p)
b.n += n
b.Flush()
}
nn += n
p = p[n:]
}
if b.err != nil {
return nn, b.err
}
// p的长度不超过buffer可用空间,写入buffer
n := copy(b.buf[b.n:], p)
b.n += n
nn += n
return nn, nil
}
分析下可以发现,上面的for循环最多执行两次,
- 第一次,如果
len(p) > b.Available(),buffer有部分数据,p截断了一部分写到buffer,然后buffer落盘; - 第二次,如果
len(p) > b.Available(),因为第一次做了Flush,所以此时buffer是空的,p直接写到磁盘;
我们继续看看Flush是怎么操作的
// Flush writes any buffered data to the underlying io.Writer.
func (b *Writer) Flush() error {
if b.err != nil {
return b.err
}
if b.n == 0 {
return nil
}
n, err := b.wr.Write(b.buf[0:b.n])
if n < b.n && err == nil {
err = io.ErrShortWrite
}
if err != nil {
if n > 0 && n < b.n {
copy(b.buf[0:b.n-n], b.buf[n:b.n])
}
b.n -= n
b.err = err
return err
}
b.n = 0
return nil
}
Flush会把buf中现有的数据写到磁盘,这里有几种情况:
n==b.n,没啥说的,buf全部写到了磁盘,直接返回;n < b.n,这意味着buf中的数据还残留了一些,设置err=io.ErrShortWrite,随后我们需要设置一下buf的大小,把残留的数据在内存中重排一下,同时更新此时的buf大小;