Go如何处理文件
参考
《Go语言入门经典》21章 处理文件
开发过程中经常会涉及到文件处理,如读取配置文件、写日志文件。下面归纳总结一下Go处理文件的方式,包括文件创建、读取、写入、删除等,对比不同的文件操作包如: ioutil
、 os
、 bufio
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.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
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方法读取子文件信息,最后按文件名对文件进行排序。
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
}

实际上是对 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
方法就使用了可读可写|创建文件|缩短文件,表示创建了文件后了文件结构体,可以通过该文件结构体对文件进行读写操作。

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 性能对比
留个坑 有时间再写吧