Go如何处理文件


Go如何处理文件

开发过程中经常会涉及到文件处理,如读取配置文件、写日志文件。下面归纳总结一下Go处理文件的方式,包括文件创建、读取、写入、删除等,对比不同的文件操作包如: ioutilosbufio



1 使用 io/ioutil

io/ioutil是标准库,但是其底层用的还是 os 包,提供的方法本质上是对 os 的封装。

  • 创建文件
  • 写入文件
  • 读取文件
  • 读取目录内容

1.1 创建&写入文件 WriteFile

WriteFile 方法在写入文件时,如果文件不存在则会自动创建文件,如果文件存在则会覆盖写入。方法有三个参数:

  • 文件名(文件路径)
  • 写入的数据(字节数组 []byte)
  • 文件模式(读写执行权限)

当写入数据为 nil 时,创建的文件为空文件;

文件模式

文件模式用四位数字表达如 0644 分别代表文件类型(普通文件)、文件所有者的权限、文件同组用户的权限、其他用户的权限。rwx分别代表读权限(4)、写权限(2)、执行权限(1),如0644中的6代表文件所有者有文件的读写(4+2)权限。

package main

import (
    "io/ioutil"
    "log"
)

func main() {
    err := ioutil.WriteFile("./example.txt", nil, 0644) // 创建空文件
    if err != nil {
        log.Fatal(err)
    }

    data := []byte("hello golang! !!")
    err = ioutil.WriteFile("./example.txt", data, 0644) // 创建文件并写入数据
    if err != nil {
        log.Fatal(err)
    }
}
os.WiteFile

内部调用的是 os.WriteFile



1.2 读取文件 ReadFile

ReadFile 方法的参数是文件名(文件路径),返回一个字节数组,将文件一次性读入内存。

data, err := ioutil.ReadFile("./example.txt")
if err != nil {
        log.Fatal(err)
}

fmt.Println(data) // [104 101 108 108 111 32 103 111 108 97 110 103 33 32 239 188 129 239 188 129]
fmt.Println(string(data)) // hello golang! !!
os.ReadFile

内部调用的是 os.ReadFile



1.3 读取目录内容 ReadDir

ReadDir 方法的参数是目录路径,返回值是目录下的文件信息,实际上目录也是一种文件。

dirData, err := ioutil.ReadDir("./")
if err != nil {
    log.Fatal(err)
}

for _, file := range dirData {
    fmt.Println(file.Name(), file.Mode(), file.Size())
}

// example.txt -rw-r--r-- 20
// float.go -rw-r--r-- 1257
// go.mod -rw-r--r-- 46
// json.go -rw-r--r-- 706
// main.go -rw-r--r-- 233
os.ReadDir

首先读目录文件,然后调用os的Readdir方法读取子文件信息,最后按文件名对文件进行排序。

FileInfo 的定义如下:

FileInfo

实际上 ioutil 包的源码一共就83行(go 1.6),也就封装了5个函数,除了上面的三个还有 ReadAll(r io.Reader)NopCloser(r io.Reader) ,后面再说。





2 使用 os

上面的io/ioutil 都是基于 os 包的封装,下面看看如何使用 os 包处理文件。源文件有 706 行(go 1.6),支持的文件操作不胜枚举,举几个基础的操作。关键是要理解这些方法操作的对象是文件结构体 File

  • 创建文件
  • 读取文件
  • 写入文件

2.1 创建文件

创建文件可以通过以下方法:

  • Create(name string)
  • OpenFile(name string, flag int, perm FileMode)

2.1.1 Create

Create(name string) 是创建文件的专用方法,参数是方法名(路径),返回值是文件结构体。

package main

import (
    "fmt"
    "log"
    "os"
)

func main() {
    f, err := os.Create("./test.txt")
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(f.Name(), f.Fd()) // ./test.txt 3
}
os.Create

实际上是对 OpenFile(name string, flag int, perm FileMode) 的封装。

返回的 File 的定义如下,其中FD是指文件描述符。

// /usr/local/go/src/os/types.go
// File represents an open file descriptor.
type File struct {
    *file // os specific
}

// /usr/local/go/src/os/file_unix.go
// file is the real representation of *File.
// The extra level of indirection ensures that no clients of os
// can overwrite this data, which could cause the finalizer
// to close the wrong file descriptor.
type file struct {
    pfd         poll.FD
    name        string
    dirinfo     *dirInfo // nil unless directory being read
    nonblock    bool     // whether we set nonblocking mode
    stdoutOrErr bool     // whether this is stdout or stderr
    appendMode  bool     // whether file is opened for appending
}

2.1.2 OpenFile

OpenFile(name string, flag int, perm FileMode)方法返回值也是文件结构体,有三个参数:

  • 文件路径
  • 文件的打开方式
  • 文件模式

文件的打开方式有:

打开方式

上面的 Create 方法就使用了可读可写|创建文件|缩短文件,表示创建了文件后了文件结构体,可以通过该文件结构体对文件进行读写操作。

os.OpenFile
f, err := os.OpenFile("./test.txt", os.O_CREATE, 0644)
if err != nil {
    log.Fatal(err)
}
fmt.Println(f.Name(), f.Fd()) // ./test.txt 3


2.2 读取文件

读取文件实际上是对文件结构体 File 进行操作,首先 Open打开文件(记得手动关闭),然后以返回的 File 对象为入口读取数据。

读文件又可以分成 带缓冲的读取不带缓冲的读取


2.2.1 不带缓冲的读取

方法一:使用 ioutil.ReadAll(r io.Reader) 读取

io.Reader 是一个声明了 Read 方法的接口,因为File 结构体实现了Read 所以可以直接作为参数(鸭子模型)。

file, err := os.Open("./example.txt")
if err != nil {
    log.Fatal(err)
}
defer file.Close()
content, err := ioutil.ReadAll(file)
if err != nil {
    log.Fatal(err)
}
fmt.Println(string(content)) // hello golang! !!

方法二:使用os.ReadFile(name string) 读取

os 本身也将上述逻辑封装成了 ReadFile(name string) ([]byte, error) 方法,这是不带缓冲的读取,即将数据一次性读入内存,其中循环将数据写入字节数组,还是看不太懂为什么得这样做(留个坑~~~)

content, err := os.ReadFile("./example.txt")
if err != nil {
    log.Fatal(err)
}
fmt.Println(string(content)) // hello golang! !!
// ReadFile reads the named file and returns the contents.
// A successful call returns err == nil, not err == EOF.
// Because ReadFile reads the whole file, it does not treat an EOF from Read
// as an error to be reported.
func ReadFile(name string) ([]byte, error) {
    f, err := Open(name)
    if err != nil {
        return nil, err
    }
    defer f.Close()

    var size int
    if info, err := f.Stat(); err == nil {
        size64 := info.Size()
        if int64(int(size64)) == size64 {
            size = int(size64)
        }
    }
    size++ // one byte for final read at EOF

    // If a file claims a small size, read at least 512 bytes.
    // In particular, files in Linux's /proc claim size 0 but
    // then do not work right if read in small pieces,
    // so an initial read of 1 byte would not work correctly.
    if size < 512 {
        size = 512
    }

    data := make([]byte, 0, size)
    for {
        if len(data) >= cap(data) {
            d := append(data[:cap(data)], 0)
            data = d[:len(data)]
        }
        n, err := f.Read(data[len(data):cap(data)])
        data = data[:len(data)+n]
        if err != nil {
            if err == io.EOF {
                err = nil
            }
            return data, err
        }
    }
}

2.2.2 带缓冲的读取

一次性读入内存,系统可能会因为文件过大而卡死,此时可以采用类似于流的形式,每次读一部分。

file, err := os.Open("./example.txt")
if err != nil {
    log.Fatal(err)
}
defer file.Close()

chunks := make([]byte, 0)
buf := make([]byte, 5) // 每次读5个字节
for {
    n, err := file.Read(buf)
    if err != nil && err != io.EOF {
        log.Fatal(err)
    }
    if n == 0 {
        break
    }
    fmt.Println(string(buf[:n]))
    chunks = append(chunks, buf[:n]...)
}
fmt.Println(string(chunks))

// hello
//  gola
// ng!!!
// hello golang!!!

针对带缓存的读写, bufio 包中封装了很多好用的函数,可以按字节读也可以按行读,上面的代码可以进行如下改写:

file, err := os.Open("./example.txt")
if err != nil {
    log.Fatal(err)
}
defer file.Close()
r := bufio.NewReader(file)

chunks := make([]byte, 0)
buf := make([]byte, 5) // 每次读五个字节
for {
    n, err := r.Read(buf)
    if err != nil && err != io.EOF {
        log.Fatal(err)
    }
    if n == 0 {
        break
    }
    fmt.Println(string(buf[:n]))
    chunks = append(chunks, buf[:n]...)
}
fmt.Println(string(chunks))

按行读文件

file, err := os.Open("./example.txt")
if err != nil {
    log.Fatal(err)
}
defer file.Close()
r := bufio.NewReader(file)

for {
    line, err := r.ReadString('\n')
    line = strings.TrimSpace(line)
    if err != nil {
        if err == io.EOF {
            fmt.Println("File read ok!")
        } else {
            fmt.Println("Read file error!", err)
        }
        break
    }
    fmt.Println(string(line))
}

// hello golang!!!
// hello bufio!!!
// File read ok!


2.3 写入文件

package main

import (
    "fmt"
    "log"
    "os"
)

func checkFileIsExist(filename string) bool {
    if _, err := os.Stat(filename); os.IsNotExist(err) {
        return false
    }
    return true
}
func main() {
    var filename = "./example.txt"
    var f *os.File
    var str = "hello os!!!\n"
    var err error

    if checkFileIsExist(filename) { //如果文件存在
        f, err = os.OpenFile(filename, os.O_WRONLY|os.O_APPEND, 0644) //打开文件
        if err != nil {
            log.Fatal(err)
        }
        fmt.Println("文件存在")
    } else {
        f, err = os.Create(filename) //创建文件
        if err != nil {
            log.Fatal(err)
        }
        fmt.Println("文件不存在")
    }
    defer f.Close()

    n, err := f.Write([]byte(str)) //写入字节数组
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("[]byte写入%d个字节\n", n)

    n, err = f.WriteString(str) //写入文件字符串
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("string写入%d个字节\n", n)

    // f.Sync() // 将文件扇入磁盘
}

也可以使用 bufio.WriteFile 改写




3 性能对比

留个坑 有时间再写吧





文章作者: xuxiangfei
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 xuxiangfei !
  目录