文件
文件
Go语言提供文件处理的标准库大致以下几个:
os
库,负责OS文件系统交互的具体实现io
库,读写IO的抽象层fs
库,文件系统的抽象层
本文会讲解如何通过Go语言来进行基本的文件处理。
打开
常见的两种打开文件的方式是使用os
包提供的两个函数,Open
函数返回值一个文件指针和一个错误,
func Open(name string) (*File, error)
后者OpenFile
能够提供更加细粒度的控制,函数Open
就是对OpenFile
函数的一个简单封装。
func OpenFile(name string, flag int, perm FileMode) (*File, error)
先来介绍第一种使用方法,直接提供对应的文件名即可,代码如下
func main() {
file, err := os.Open("README.txt")
fmt.Println(file, err)
}
文件的查找路径默认为项目go.mod
文件所在的路径,由于项目下并没有文件README.txt
,所以自然会返回一个错误。
<nil> open README.txt: The system cannot find the file specified.
因为IO错误的类型有很多,所以需要手动的去判断文件是否存在,同样的os
包也为此提供了方便函数,修改后的代码如下
func main() {
file, err := os.Open("README.txt")
if os.IsNotExist(err) {
fmt.Println("文件不存在")
} else if err != nil {
fmt.Println("文件访问异常")
} else {
fmt.Println("文件读取成功", file)
}
}
再次运行输出如下
文件不存在
事实上第一种函数读取的文件仅仅只是只读的,无法被修改
func Open(name string) (*File, error) {
return OpenFile(name, O_RDONLY, 0)
}
通过OpenFile
函数可以控制更多细节,例如修改文件描述符和文件权限,关于文件描述符,os
包下提供了以下常量以供使用。
const (
// 只读,只写,读写 三种必须指定一个
O_RDONLY int = syscall.O_RDONLY // 以只读的模式打开文件
O_WRONLY int = syscall.O_WRONLY // 以只写的模式打开文件
O_RDWR int = syscall.O_RDWR // 以读写的模式打开文件
// 剩余的值用于控制行为
O_APPEND int = syscall.O_APPEND // 当写入文件时,将数据添加到文件末尾
O_CREATE int = syscall.O_CREAT // 如果文件不存在则创建文件
O_EXCL int = syscall.O_EXCL // 与O_CREATE一起使用, 文件必须不存在
O_SYNC int = syscall.O_SYNC // 以同步IO的方式打开文件
O_TRUNC int = syscall.O_TRUNC // 当打开的时候截断可写的文件
)
关于文件权限的则提供了以下常量。
const (
ModeDir = fs.ModeDir // d: 目录
ModeAppend = fs.ModeAppend // a: 只能添加
ModeExclusive = fs.ModeExclusive // l: 专用
ModeTemporary = fs.ModeTemporary // T: 临时文件
ModeSymlink = fs.ModeSymlink // L: 符号链接
ModeDevice = fs.ModeDevice // D: 设备文件
ModeNamedPipe = fs.ModeNamedPipe // p: 具名管道 (FIFO)
ModeSocket = fs.ModeSocket // S: Unix 域套接字
ModeSetuid = fs.ModeSetuid // u: setuid
ModeSetgid = fs.ModeSetgid // g: setgid
ModeCharDevice = fs.ModeCharDevice // c: Unix 字符设备, 前提是设置了 ModeDevice
ModeSticky = fs.ModeSticky // t: 黏滞位
ModeIrregular = fs.ModeIrregular // ?: 非常规文件
// 类型位的掩码. 对于常规文件而言,什么都不会设置.
ModeType = fs.ModeType
ModePerm = fs.ModePerm // Unix 权限位, 0o777
)
下面是一个以读写模式打开一个文件的代码例子,权限为0666
,表示为所有人都可以对该文件进行读写,且不存在时会自动创建。
func main() {
file, err := os.OpenFile("README.txt", os.O_RDWR|os.O_CREATE, 0666)
if os.IsNotExist(err) {
fmt.Println("文件不存在")
} else if err != nil {
fmt.Println("文件访问异常")
} else {
fmt.Println("文件打开成功", file.Name())
file.Close()
}
}
输出如下
文件打开成功 README.txt
倘若只是想获取该文件的一些信息,并不想读取该文件,可以使用os.Stat()
函数进行操作,代码示例如下
func main() {
fileInfo, err := os.Stat("README.txt")
if err != nil {
fmt.Println(err)
} else {
fmt.Println(fmt.Sprintf("%+v", fileInfo))
}
}
输出如下
&{name:README.txt FileAttributes:32 CreationTime:{LowDateTime:3603459389 HighDateTime:31016791} LastAccessTime:{LowDateTime:3603459389 HighDateTime:31016791} LastWriteTime:{LowDateTime:3603459389 HighDateTime:31016791} FileSizeHigh
:0 FileSizeLow:0 Reserved0:0 filetype:0 Mutex:{state:0 sema:0} path:README.txt vol:0 idxhi:0 idxlo:0 appendNameToPath:false}
注意
打开一个文件后永远要记得关闭该文件,通常关闭操作会放在defer
语句里
defer file.Close()
读取
当成功的打开文件后,便可以进行读取操作了,关于读取文件的操作,*os.File
类型提供了以下几个公开的方法
// 将文件读进传入的字节切片
func (f *File) Read(b []byte) (n int, err error)
// 相较于第一种可以从指定偏移量读取
func (f *File) ReadAt(b []byte, off int64) (n int, err error)
大多数情况第一种使用的较多。针对于第一种方法,需要自行编写逻辑来进行读取时切片的动态扩容,代码如下
func ReadFile(file *os.File) ([]byte, error) {
buffer := make([]byte, 0, 512)
for {
// 当容量不足时
if len(buffer) == cap(buffer) {
// 扩容
buffer = append(buffer, 0)[:len(buffer)]
}
// 继续读取文件
offset, err := file.Read(buffer[len(buffer):cap(buffer)])
// 将已写入的数据归入切片
buffer = buffer[:len(buffer)+offset]
// 发生错误时
if err != nil {
if errors.Is(err, io.EOF) {
err = nil
}
return buffer, err
}
}
}
剩余逻辑如下
func main() {
file, err := os.OpenFile("README.txt", os.O_RDWR|os.O_CREATE, 0666)
if err != nil {
fmt.Println("文件访问异常")
} else {
fmt.Println("文件打开成功", file.Name())
bytes, err := ReadFile(file)
if err != nil {
fmt.Println("文件读取异常", err)
} else {
fmt.Println(string(bytes))
}
file.Close()
}
}
输出为
文件打开成功 README.txt
hello world!
除此之外,还可以使用两个方便函数来进行文件读取,分别是os
包下的ReadFile
函数,以及io
包下的ReadAll
函数。对于os.ReadFile
而言,只需要提供文件路径即可,而对于io.ReadAll
而言,则需要提供一个io.Reader
类型的实现,
os.ReadFile
func ReadFile(name string) ([]byte, error)
使用例子如下
func main() {
bytes, err := os.ReadFile("README.txt")
if err != nil {
fmt.Println(err)
} else {
fmt.Println(string(bytes))
}
}
输出如下
hello world!
io.ReadAll
func ReadAll(r Reader) ([]byte, error)
使用例子如下
func main() {
file, err := os.OpenFile("README.txt", os.O_RDWR|os.O_CREATE, 0666)
if err != nil {
fmt.Println("文件访问异常")
} else {
fmt.Println("文件打开成功", file.Name())
bytes, err := io.ReadAll(file)
if err != nil {
fmt.Println(err)
} else {
fmt.Println(string(bytes))
}
file.Close()
}
}
文件打开成功 README.txt
hello world!
写入
os.File
结构体提供了以下几种方法以供写入数据
// 写入字节切片
func (f *File) Write(b []byte) (n int, err error)
// 写入字符串
func (f *File) WriteString(s string) (n int, err error)
// 从指定位置开始写,当以os.O_APPEND模式打开时,会返回错误
func (f *File) WriteAt(b []byte, off int64) (n int, err error)
如果想要对一个文件写入数据,则必须以O_WRONLY
或O_RDWR
的模式打开,否则无法成功写入文件。下面是一个以os.O_RDWR|os.O_CREATE|os.O_APPEND|os.O_TRUNC
模式打开文件,且权限为0666
向指定写入数据的例子
func main() {
file, err := os.OpenFile("README.txt", os.O_RDWR|os.O_CREATE|os.O_APPEND|os.O_TRUNC, 0666)
if err != nil {
fmt.Println("文件访问异常")
} else {
fmt.Println("文件打开成功", file.Name())
for i := 0; i < 5; i++ {
offset, err := file.WriteString("hello world!\n")
if err != nil {
fmt.Println(offset, err)
}
}
fmt.Println(file.Close())
}
}
由于是以os.O_APPEND
模式打开的文件,所以在写入文件时会将数据添加到文件尾部,执行完毕后文件内容如下
hello world!
hello world!
hello world!
hello world!
hello world!
向文件写入字节切片也是类似的操作,就不再赘述。对于写入文件的操作标准库同样提供了方便函数,分别是os.WriteFile
与io.WriteString
os.WriteFile
func WriteFile(name string, data []byte, perm FileMode) error
使用例子如下
func main() {
err := os.WriteFile("README.txt", []byte("hello world!\n"), 0666)
if err != nil {
fmt.Println(err)
}
}
此时文件内容如下
hello world!
io.WriteString
func WriteString(w Writer, s string) (n int, err error)
使用例子如下
func main() {
file, err := os.OpenFile("README.txt", os.O_RDWR|os.O_CREATE|os.O_APPEND|os.O_TRUNC, 0666)
if err != nil {
fmt.Println("文件访问异常")
} else {
fmt.Println("文件打开成功", file.Name())
for i := 0; i < 5; i++ {
offset, err := io.WriteString(file, "hello world!\n")
if err != nil {
fmt.Println(offset, err)
}
}
fmt.Println(file.Close())
}
}
hello world!
hello world!
hello world!
hello world!
hello world!
函数os.Create
函数用于创建文件,本质上也是对OpenFile
的封装。
func Create(name string) (*File, error) {
return OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0666)
}
注意
在创建一个文件时,如果其父目录不存在,将创建失败并会返回错误。
复制
对于复制文件而言,需要同时打开两个文件,第一种方法是将原文件中的数据读取出来,然后写入目标文件中,代码示例如下
func main() {
// 从原文件中读取数据
data, err := os.ReadFile("README.txt")
if err != nil {
fmt.Println(err)
return
}
// 写入目标文件
err = os.WriteFile("README(1).txt", data, 0666)
if err != nil {
fmt.Println(err)
} else {
fmt.Println("复制成功")
}
}
*os.File.ReadFrom
另一种方法是使用os.File
提供的方法ReadFrom
,打开文件时,一个只读,一个只写。
func (f *File) ReadFrom(r io.Reader) (n int64, err error)
使用示例如下
func main() {
// 以只读的方式打开原文件
origin, err := os.OpenFile("README.txt", os.O_RDONLY, 0666)
if err != nil {
fmt.Println(err)
return
}
defer origin.Close()
// 以只写的方式打开副本文件
target, err := os.OpenFile("README(1).txt", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
if err != nil {
fmt.Println(err)
return
}
defer target.Close()
// 从原文件中读取数据,然后写入副本文件
offset, err := target.ReadFrom(origin)
if err != nil {
fmt.Println(err)
return
}
fmt.Println("文件复制成功", offset)
}
这种复制方式需要先将源文件的全部内容读取到内存中,再写入目标文件,文件特别大的时候不建议这么做。
io.Copy
另一种方法就是使用io.Copy
函数,它则是一边读一边写,先将内容读到缓冲区中,再写入到目标文件中,缓冲区默认大小为32KB。
func Copy(dst Writer, src Reader) (written int64, err error)
使用示例如下
func main() {
// 以只读的方式打开原文件
origin, err := os.OpenFile("README.txt", os.O_RDONLY, 0666)
if err != nil {
fmt.Println(err)
return
}
defer origin.Close()
// 以只写的方式打开副本文件
target, err := os.OpenFile("README(1).txt", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
if err != nil {
fmt.Println(err)
return
}
defer target.Close()
// 复制
written, err := io.Copy(target, origin)
if err != nil {
fmt.Println(err)
} else {
fmt.Println(written)
}
}
你也可以使用io.CopyBuffer
来指定缓冲区大小。
重命名
重命名也可以理解为移动文件,会用到os
包下的Rename
函数。
func Rename(oldpath, newpath string) error
示例如下
func main() {
err := os.Rename("README.txt", "readme.txt")
if err != nil {
fmt.Println(err)
} else {
fmt.Println("重命名成功")
}
}
该函数对于文件夹也是同样的效果。
删除
删除操作相较于其他操作要简单的多,只会用到os
包下的两个函数
// 删除单个文件或者空目录,当目录不为空时会返回错误
func Remove(name string) error
// 删除指定目录的所有文件和目录包括子目录与子文件
func RemoveAll(path string) error
使用起来十分的简单,下面是删除目录的例子
func main() {
// 删除当前目录下所有的文件与子目录
err := os.RemoveAll(".")
if err != nil {
fmt.Println(err)
}else {
fmt.Println("删除成功")
}
}
下面是删除单个文件的例子
func main() {
// 删除当前目录下所有的文件与子目录
err := os.Remove("README.txt")
if err != nil {
fmt.Println(err)
} else {
fmt.Println("删除成功")
}
}
刷新
os.Sync
这一个函数封装了底层的系统调用Fsync
,用于将操作系统中缓存的IO写入落实到磁盘上
func main() {
create, err := os.Create("test.txt")
if err != nil {
panic(err)
}
defer create.Close()
_, err = create.Write([]byte("hello"))
if err != nil {
panic(err)
}
// 刷盘
if err := create.Sync();err != nil {
return
}
}
文件夹
文件夹的许多操作都与文件操作类似
读取
对于文件夹而言,打开方式有两种,
os.ReadDir
第一种方式是使用os.ReadDir
函数
func ReadDir(name string) ([]DirEntry, error)
func main() {
// 当前目录
dir, err := os.ReadDir(".")
if err != nil {
fmt.Println(err)
} else {
for _, entry := range dir {
fmt.Println(entry.Name())
}
}
}
*os.File.ReadDir
第二种方式是使用*os.File.ReadDir
函数,os.ReadDir
本质上也只是对*os.File.ReadDir
的一层简单封装。
// n < 0时,则读取文件夹下所有的内容
func (f *File) ReadDir(n int) ([]DirEntry, error)
func main() {
// 当前目录
dir, err := os.Open(".")
if err != nil {
fmt.Println(err)
}
defer dir.Close()
dirs, err := dir.ReadDir(-1)
if err != nil {
fmt.Println(err)
} else {
for _, entry := range dirs {
fmt.Println(entry.Name())
}
}
}
创建
创建文件夹操作会用到os
包下的两个函数
// 用指定的权限创建指定名称的目录
func Mkdir(name string, perm FileMode) error
// 相较于前者该函数会创建一切必要的父目录
func MkdirAll(path string, perm FileMode) error
示例如下
func main() {
err := os.Mkdir("src", 0666)
if err != nil {
fmt.Println(err)
} else {
fmt.Println("创建成功")
}
}
复制
我们可以自己写函数递归遍历整个文件夹,不过filepath
标准库已经提供了类似功能的函数,所以可以直接使用,一个简单的文件夹复制的代码示例如下。
func CopyDir(src, dst string) error {
// 检查源文件夹的状态
_, err := os.Stat(src)
if err != nil {
return err
}
return filepath.Walk(src, func(path string, info fs.FileInfo, err error) error {
if err != nil {
return err
}
// 计算相对路径
rel, err := filepath.Rel(src, path)
if err != nil {
return err
}
// 拼接目标路径
destpath := filepath.Join(dst, rel)
// 创建文件夹
var dirpath string
var mode os.FileMode = 0755
if info.IsDir() {
dirpath = destpath
mode = info.Mode()
} else if info.Mode().IsRegular() {
dirpath = filepath.Dir(destpath)
}
if err := os.MkdirAll(dirpath, mode); err != nil {
return err
}
// 创建文件
if info.Mode().IsRegular() {
srcfile, err := os.Open(path)
if err != nil {
return err
}
// 一定要记得关闭文件
defer srcfile.Close()
destfile, err := os.OpenFile(destpath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, info.Mode())
if err != nil {
return err
}
defer destfile.Close()
// 复制文件内容
if _, err := io.Copy(destfile, srcfile); err != nil {
return err
}
return nil
}
return nil
})
}
filepath.Walk
会递归遍历整个文件夹,在过程中,遇到文件夹就创建文件夹,遇到文件就创建新文件并复制,代码相比复制文件有点多但算不上复杂。