重学Go语言 | 一文详解Go文件操作

公众号:程序员读书;欢迎关注

有很多场景都需要对文件进行读取或者写入,比如读取配置文件或者写入日志文件,除此之外,有时候我们也需要修改文件名称,遍历目录内的文件,删除文件,在Go语言中,操作文件应该算是一件比较简单的事情,我们在这一篇文章中,一起来探究一下。

文件句柄:os.File

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

os.Create

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

go 复制代码
file,err := os.Create("./my.dat")

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

os.Open

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

go 复制代码
file, err := os.Open("./my.dat")

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

os.OpenFile

其实,os.Create()函数和os.Open()函数的底层都是调用os.OpenFile()函数,这一点从os.Creat()os.Open()函数的源码可以得到证实:

go 复制代码
//该源码位于标准库os包下的file.go文件中
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()函数签名如下:

go 复制代码
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函数时就传入多个标志:

go 复制代码
OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0666)

os.OpenFile()函数的第三个参数 FileMode就是指类Unix操作系统中的文件读写权限,即r(读)、w(写),x(执行),一个文件的rwx有三组:

go 复制代码
-rw-r--r--   1 root  staff  215  4 17 11:14 main.go

-rw-r--r--分别表示文件拥有者、拥有者所在群组、其他人对该文件的权限,如果没有该权限用-表示。

rwx用八进制表示时r=4,w=2,x=1,所以上面main.go文件权限用八进制表示为0644

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

go 复制代码
name := "./my.dat"
file,err := OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0755) 
if err != nil{
  panic(err)
}
defer file.Close()

读取文件

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

go 复制代码
file, err := os.OpenFile("./my.dat", os.O_RDONLY, 0)

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

直接读取

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

go 复制代码
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))
	}
}

在上面的例子中,我们循环读取文件内容,直到读取到文件末尾时,返回的字节长度0和一个io.EOF的error类型,这时候表示文件已经读完,可以结束读取了。

使用bufio包读取文件

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

go 复制代码
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方法的使用示例:

go 复制代码
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))
	}
}

使用fmt包读取文件

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

go 复制代码
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方法的使用示例:

go 复制代码
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
		}
	}
}

使用ioutil包读取文件

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

go 复制代码
f, err := os.Open("./my.dat")
if err != nil {
		panic(err)
}
var b []byte
b,err := ioutil.ReadAll(f)

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

go 复制代码
var b []byte
b,err := ioutil.ReadFile("./my.dat")

写入文件

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

go 复制代码
OpenFile(name, O_RDWR|O_CREATE|O_APPEND, 0666)

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

直接写入

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

go 复制代码
package main

import "os"

func main() {

	file, err := os.OpenFile("./my.dat", os.O_RDWR, 0666)

	if err != nil {
		panic(file)
	}
	defer file.Close()
	file.Write([]byte("test222222"))
}

也可以调用os.FileWriteString直接写入一个字符串:

go 复制代码
file.WriteString("test222222")

使用bufio包写入文件

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

go 复制代码
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)

上面几个方法似乎与io.File自身所拥有的方法差别不大,但bufio包的写入是带有缓冲区的,也就是说当我们写入数据时,不是立刻写入到文件,而是写到内存缓冲区,最后调用Flush方法才将数据写入文件。

go 复制代码
package main

import (
	"bufio"
	"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("111111111"))
	writer.Flush()
}

使用fmt包写入文件

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

go 复制代码
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写入文件的示例:

go 复制代码
package main

import (
	"fmt"
	"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", "test")
}

使用ioutil包写入文件

同样的,ioutil包也对写入文件做了封装,使用一个函数便可以完成上码代码要完成的事情:

go 复制代码
ioutil.WriteFile("./my.dat", []byte("22222"), 0666)

判断是否为目录

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

go 复制代码
type FileInfo interface {
	Name() string       // base name of the file
	Size() int64        // length in bytes for regular files; system-dependent for others
	Mode() FileMode     // file mode bits
	ModTime() time.Time // modification time
	IsDir() bool        // abbreviation for Mode().IsDir()
	Sys() any           // underlying data source (can return nil)
}

示例:

go 复制代码
fileInfo, err := file.Stat()

if fileInfo.IsDir() {
	fmt.Println(fileInfo.Name())
}

遍历目录

如果想遍历目录,可以调用os.ReadDir()函数,该函数返回一个元素类型为os.DirEntry的切片:

go 复制代码
func (f *File) ReadDir(n int) ([]DirEntry, error)

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

go 复制代码
type DirEntry interface {
	Name() string
	IsDir() bool
	Type() FileMode
	Info() (FileInfo, error)
}

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

go 复制代码
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())
		}
	}
}

修改文件名称

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

go 复制代码
err := os.Rename("./1.txt", "./2.txt")

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

go 复制代码
err := os.Rename("./1.txt", "./m/2.txt")

删除文件

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

go 复制代码
fileName := "./1.txt"
os.Remove(fileName)

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

go 复制代码
//m为当前目录下一个非空目录
err := os.Remove("./m")
fmt.Println(err)

执行结果:

arduino 复制代码
remove ./m: directory not empty

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

go 复制代码
err := os.RemoveAll("./m")

小结

好了,至此,相信你对于在Go语言如何读取和写入文件应该掌握了吧,总结一下,在这篇文章中,主要讲了以下几点:

  • 如何获得一个文件句柄os.File
  • 如何以不同的方式读取文件内容。
  • 如何以不同的方式向文件写入内容。
  • 对文件的不同操作。
相关推荐
小突突突40 分钟前
Spring框架中的单例bean是线程安全的吗?
java·后端·spring
iso少年44 分钟前
Go 语言并发编程核心与用法
开发语言·后端·golang
掘金码甲哥1 小时前
云原生算力平台的架构解读
后端
码事漫谈1 小时前
智谱AI从清华实验室到“全球大模型第一股”的六年征程
后端
码事漫谈1 小时前
现代软件开发中常用架构的系统梳理与实践指南
后端
Mr.Entropy1 小时前
JdbcTemplate 性能好,但 Hibernate 生产力高。 如何选择?
java·后端·hibernate
何贤1 小时前
2025 年终回顾:25 岁,从“混吃等死”到别人眼中的“技术专家”
前端·程序员·年终总结
YDS8292 小时前
SpringCloud —— MQ的可靠性保障和延迟消息
后端·spring·spring cloud·rabbitmq
无限大62 小时前
为什么"区块链"不只是比特币?——从加密货币到分布式应用
后端
洛神么么哒2 小时前
freeswitch-初级-01-日志分割
后端