go写文件的几种姿势

348 阅读3分钟

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中现有的数据写到磁盘,这里有几种情况:

  1. n==b.n,没啥说的,buf全部写到了磁盘,直接返回;
  2. n < b.n,这意味着buf中的数据还残留了一些,设置err=io.ErrShortWrite,随后我们需要设置一下buf的大小,把残留的数据在内存中重排一下,同时更新此时的buf大小;