Go语言的文件操作

1. 文件句柄:os.File

Go语言的标准库os包下的File结构体表示一个文件句柄,获得了os.File,就可以对文件进行各种操作,有以下三种方式获得os.File文件句柄:

1.1 os.Create

通过os.Create()函数传入一个文件名称可以创建并获得一个表示该文件的os.File结构体

1
file,err := os.Create("./my.dat")

如果指定的文件不存在,调用该函数后,会创建文件,如果文件已经存在,则只会清空文件的内容。

1.2 os.Open

对于已经存在的文件,如果不想清空文件内容,只想打开该文件的话,可以用os.Open()函数:

1
file, err := os.Open("./my.dat")

用该函数打开一个文件句柄时,如果文件不存在,返回值err返回一个error类型的错误。

1.3 os.OpenFile

其实,os.Create()函数和os.Open()函数的底层都是调用os.OpenFile()函数

1
2
3
4
5
6
7
func Open(name string) (*File, error) {
return OpenFile(name, O_RDONLY, 0)
}

func Create(name string) (*File, error) {
return OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0666)
}

os.OpenFile()函数签名如下:

1
func OpenFile(name string, flag int, perm FileMode) (*File, error) 

从函数签名可以看到调用os.OpenFile函数时要传入三个参数,其中name是表示要打开的文件名。

而第二个参数flag表示打开文件的标志,比较常用有以下几种取值:

  • O_RDONLY:只读
  • O_WRONLY:只写
  • O_RDWR:读写
  • O_APPEND:以追加的方式写入
  • O_CREATE:文件不存在时创建
  • O_TRUNC:当文件存在时,将文件内容置空

可以同时指定多个标志,多个标志用逻辑运算符|连接起来,比如os.Create()函数在调用os.OpenFile函数时就传入多个标志:

1
OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0666)

无论是用哪种方式打开的文件句柄,最终都要记得关闭以释放资源,比较标准的用法是用defer语句:

1
2
3
4
5
6
name := "./my.dat"
file,err := OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0755)
if err != nil{
panic(err)
}
defer file.Close()

2. 读取文件

要读取文件的内容,在获得文件句柄时,OpenFile函数的参数flag只需要传O_RDONLY就可以了,而参数FileMode可以为0:

1
file, err := os.OpenFile("./my.dat", os.O_RDONLY, 0)

os.File有一个Read()方法,也就是说os.File实现io.Reader接口,Go标准库很多的包可以处理io.Reader接口,比如ioutil,bufio,fmt等,因此有很多种方式可以读取文件的内容。

2.1 直接读取

os.FileRead方法就可以直接将文件内容读取一个字节数组中,并返回读取的长度和一个用于判断是否出错的error类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
"fmt"
"io"
"os"
)

func main() {
f, err := os.Open("./my.dat")
if err != nil {
panic(err)
}
defer f.Close()
for {
b := make([]byte, 10)
n, err := f.Read(b) //将文件内容读取到字节数组中
if n == 0 || err == io.EOF {
return
}
fmt.Println(n, string(b))
}
}

2.2 使用bufio 包读取

当要用bufio包读取文件时,将调用bufio.NewReader()函数将io.Reader包装为一个bufio.Reader结构体,该结构体封装了很多更便捷读取文件的方法:

1
2
3
4
5
6
7
func (b *Reader) Read(p []byte) (n int, err error)
func (b *Reader) ReadByte() (byte, error)
func (b *Reader) ReadBytes(delim byte) ([]byte, error)
func (b *Reader) ReadLine() (line []byte, isPrefix bool, err error)
func (b *Reader) ReadRune() (r rune, size int, err error)
func (b *Reader) ReadSlice(delim byte) (line []byte, err error)
func (b *Reader) ReadString(delim byte) (string, error)

ReadLine方法的使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import (
"bufio"
"fmt"
"io"
"os"
)

func main() {
file, err := os.OpenFile("./my.dat", os.O_RDWR, 0666)
if err != nil {
panic(err)
}
defer file.Close()
reader := bufio.NewReader(file)
for {
//按行读取
b, _, err := reader.ReadLine()
if err == io.EOF {
break
}
fmt.Println(string(b))
}
}

2.3 使用fmt包读取文件

fmt包以FScan...开头的函数可以按一定的格式扫描读取文件里的内容:

1
2
3
func Fscan(r io.Reader, a ...any) (n int, err error)
func Fscanf(r io.Reader, format string, a ...any) (n int, err error)
func Fscanln(r io.Reader, a ...any) (n int, err error)

Fscanln方法的使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
"fmt"
"io"
"os"
)

func main() {
file, err := os.OpenFile("./my.dat", os.O_RDWR, 0666)
if err != nil {
panic(err)
}
defer file.Close()
for {
var a1, a2 string
_, err := fmt.Fscanln(file, &a1, &a2)
fmt.Println(a2, a2)
if err == io.EOF {
break
}
}
}

2.4 使用ioutil包读取文件

标准库的ioutil包对读取文件做好封装,可以直接读取整个文件的数据:

1
2
3
4
5
6
f, err := os.Open("./my.dat")
if err != nil {
panic(err)
}
var b []byte
b,err := ioutil.ReadAll(f)

ioutil甚至封装了直接读取文件的函数:

1
2
var b []byte
b,err := ioutil.ReadFile("./my.dat")

3. 写入文件

要向文件写入内容,在调用OpenFile()函数获得句柄时flag参数要传入O_WRONLY或者O_RDWR,如果是要往文件中以追加的形式在文件后面插入内容,还是需要O_APPEND:

1
OpenFile(name, O_RDWR|O_CREATE|O_APPEND, 0666)

os.FileWrite方法,也就是说os.File也实现了io.Writer接口,所以同样可以调用fmtbufioioutil包将数据写入到文件中。

3.1 直接写入

写入文件最简单的方式就是调用os.File类型的Write方法写入一个字节数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import (
"fmt"
"io"
"os"
)

func main() {
file, err := os.OpenFile("./my.dat", os.O_RDWR|os.O_CREATE, 0666)
if err != nil {
panic(err)
}
defer file.Close()
file.WriteString("hello moka\n")
file.Seek(0, 0)
for {
var a1, a2 string
_, err := fmt.Fscanln(file, &a1, &a2)
fmt.Println(a1, a2)
if err == io.EOF {
break
}
}
}

3.2 使用bufio包写入文件

bufio包的NewWriter可以将一个io.Writer包装为bufio.Writer结构体,该结构体主要有以下几个方法可以将数据写入文件:

1
2
3
4
func (b *Writer) Write(p []byte) (nn int, err error)
func (b *Writer) WriteByte(c byte) error
func (b *Writer) WriteRune(r rune) (size int, err error)
func (b *Writer) WriteString(s string) (int, error)

bufio包的写入是带有缓冲区的,也就是说当写入数据时,不是立刻写入到文件,而是写到内存缓冲区,最后调用Flush方法才将数据写入文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import (
"bufio"
"fmt"
"io"
"os"
)


func main() {
file, err:= os.OpenFile("./my.dat", os.O_RDWR, 0666)
if err != nil {
panic(err)
}
defer file.Close()
writer := bufio.NewWriter(file)

writer.Write([]byte("hello mocha"))
writer.Flush()
file.Seek(0, 0)
for {
var a1, a2 string
_, err := fmt.Fscanln(file, &a1, &a2)
fmt.Println(a1, a2)
if err == io.EOF {
break
}
}
}

3.3 使用fmt包写入文件

fmt包以下三个函数可以将格式化的数据写入到一个io.Writer:

1
2
3
func Fprint(w io.Writer, a ...any) (n int, err error)
func Fprintf(w io.Writer, format string, a ...any) (n int, err error)
func Fprintln(w io.Writer, a ...any) (n int, err error)

使用fmt写入文件的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main

import (
"fmt"
"io"
"os"
)


func main() {
file, err := os.OpenFile("./my.dat", os.O_RDWR, 0666)
if err != nil {
panic(err)
}
defer file.Close()
fmt.Fprintf(file, "%s:%s", "username", "moka")
file.Seek(0, 0)
for {
var a1, a2 string
_, err := fmt.Fscanln(file, &a1, &a2)
fmt.Println(a1, a2)
if err == io.EOF {
break
}
}
}

4. 判断是否为目录

要判断文件是否为目录,在获得os.File对象,可以调用该对象的Stat方法,该返回返回一个实现了os.FileInfo接口的对象:

1
2
3
4
5
6
7
8
type FileInfo interface {
Name() string // 文件的基本名称
Size() int64 // 普通文件的字节长度;其他文件的系统相关长度
Mode() FileMode // 文件模式位
ModTime() time.Time // 修改时间
IsDir() bool // Mode().IsDir()的缩写
Sys() any // 底层数据源(可以返回nil)
}

比如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main  

import (
"fmt"
"os"
)

func main() {
filePath := "/path/to/file"

// 打开文件
fileInfo, err := os.Stat(filePath)
if err != nil {
fmt.Println("无法获取文件信息:", err)
return
}

// 判断是否为目录
if fileInfo.IsDir() {
fmt.Println("文件是一个目录")
} else {
fmt.Println("文件不是一个目录")
}
}

5. 遍历目录

可以调用os.ReadDir()函数,该函数返回一个元素类型为os.DirEntry的切片:

1
func (f *File) ReadDir(n int) ([]DirEntry, error)

os.DirEntry是一个接口,其定义如下:

1
2
3
4
5
6
type DirEntry interface {
Name() string
IsDir() bool
Type() FileMode
Info() (FileInfo, error)
}

DirEntry接口也有IsDir()方法,因为可以再往下遍历,下面是一个实现目录遍历的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

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


func main() {
base := "./"
IterateFolder(base)
}

func IterateFolder(base string) {
dirEntry, err := os.ReadDir(base)
if err != nil {
log.Fatal(err)
}
for _, v := range dirEntry {
if v.IsDir() {
IterateFolder(base + "/" + v.Name())
} else {
fmt.Println(v.Name())
}
}
}

6. 修改文件名称

修改文件名称用os.Rename函数:

1
err := os.Rename("./demo1.txt", "./demo2.txt")

os.Rename函数也可以用于移动文件:

1
err := os.Rename("./hello.txt", "./m/hello.txt")

7. 删除文件

删除一个文件或者一个空目录,直接调用os包的Remove()函数即可:

1
2
fileName := "./1.txt"
os.Remove(fileName)

可以根据error的返回值是否为nil判断是否删除成功,比如我们删除一个不存在的文件或者删除一个非空的目录:

1
2
3
//m为当前目录下一个非空目录
err := os.Remove("./m")
fmt.Println(err)

执行结果:

1
remove ./m: directory not empty

对于非空目录,如果要删除,可以用os包的RemoveAll函数:

1
err := os.RemoveAll("./m")

参考

  1. https://juejin.cn/post/7264781288275394572
  2. https://juejin.cn/post/7260904509198762021?searchId=20230915030002D063C3D51EC1D87BE2B3#heading-0

Go语言的文件操作
https://suiyideali.github.io/2023/09/11/Go语言的文件操作/
作者
m0ch4z
发布于
2023年9月11日
更新于
2023年9月15日
许可协议