文件

寒江蓑笠翁大约 21 分钟

文件

Go文件操作的基础数据类型支持是[]byte,即字节切片,标准库相关的包有很多,例如osio等,接下来会介绍关于文件的基本操作。

注意

由于ioutil包已经被弃用,不建议继续使用。


打开

常见的两种打开文件的方式是使用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文件所在的路径,由于项目下并没有该文件,所以自然会返回一个错误。

<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)
	}
}

再次运行输出如下

文件不存在

事实上第一种函数读取的文件仅仅只是只读的,无法被修改,Open函数内部实现

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
)

提示

truncates意思即为将文件容量调整到合适的大小以容纳数据,不大也不小。

下面是一个以读写模式打开一个文件的代码例子,权限为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.Lstat()函数进行操作,代码示例如下

func main() {
	fileInfo, err := os.Lstat("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_WRONLYO_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.WriteFileio.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方便函数

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)
	}
}

重命名

重命名也可以理解为移动文件,会用到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会递归遍历整个文件夹,在过程中,遇到文件夹就创建文件夹,遇到文件就创建新文件并复制,代码相比复制文件有点多但算不上复杂。