原文地址
摘要
在任何语言中,读写磁盘和浏览文件系统都是重点内容。让我们使用os包来学习go如何读写IO ,os包是一个能让我们与操作系统功能交互的包。
Files
创建和打开文件
文件创建可以用os.Create完成。用os.Open创建并打开一个文件。两者都接受一个文件路径,并返回一个File结构体,如果不成功则返回一个非空错误。
import (
"os"
)
func createFile(){
filePtr, err := os.Create("./test/creating.txt");
if err != nil {
log.Fatal(err);
}
defer filePtr.Close(); // close the file
// We can read from and write to the file
}
func openFile(){
filePtr, err := os.Open("./test/creating.txt");
if err != nil {
log.Fatal(err);
}
defer filePtr.Close(); // close the file
// We can read from and write to the file
}
如果文件已经存在你调用os.Create,它将截断文件:文件的数据将被删除。相反,调用os.Open传入一个不存在的文件将导致错误返回。
如果成功,我们就可以使用返回的File结构体向文件写入和读取数据(在下一节中将会有一个打开文件、一块一块地读取、然后关闭文件的例子)。
最后,在使用完文件后,调用File.Close关闭文件。
读文件
一种处理文件的方式就是一次性将文件所有内容读取到内存中。可以使用os.ReadFile。函数参数是文件路径,输出是文件内容的字节数组,如果读取失败会返回错误。
import (
"log"
"os"
)
/*
Contents of test.txt:
Hello World!
*/
func readFileContents(){
bytes, err := os.ReadFile("test.txt");
if err != nil {
log.Fatal(err);
}
fileText = string(bytes[:]); // fileText is "Hello World!"
}
如果读取的是一个文本文件,需要将字节数组转换为字符串。
记住os.ReadFile是将整个文件读取到内存当中。如果文件很大的话,使用os.ReadFile将消耗大量内存。
一种内存性能更好的读取方式就是分片读取,可以使用os.Open。
func readFileChunkWise() {
chunkSize := 10 // processing the file 10 bytes at a time
b := make([]byte, chunkSize)
file, err := os.Open("./folder/test.txt);
if err != nil {
log.Fatal(err)
}
for {
bytesRead, _ := file.Read(b);
if bytesRead == 0 { // bytesRead will be 0 at the end of the file.
break
}
// process the current bytes read
process(b, bytesRead);
}
file.Close();
}
打开文件后,File.Read不断读取文件直到遇到EOF(文件结束符)。
File.Read需要字节数组作为参数,并读取数据到长度等于字节数组b中。然后返回读取的字节数bytesRead,如果读取失败会返回错误。如果bytesRead是0,意味着遇到EOF,读取文件结束。
在上面的代码中,从文件中读取10个字节内容,然后处理字节数组,重复处理直到文件结束。对于大文件,这种方式使用更少内存。
写文件/追加内容到文件
与os.ReadFile对应有个os.WriteFile方法,用来写数据到文件。
import (
"log"
"os"
)
func writeFileContents() {
content := "Something to write";
/* os.WriteFile takes in file path, a []byte of the file content,
and permission bits in case file doesn't exist */
err := os.WriteFile("./test.txt", []byte(content), 0666);
if err != nil {
log.Fatal(err);
}
}
关于os.WriteFile需要注意点:
- 确保写入文件的内容转换为[]byte类型,然后调用os.Write
- 权限位需要设置,用于创建文件如果文件不存在。
- 如果文件已经存在,os.WriteFile将新写入的内容覆盖原先的文件。
os.WriteFile很方便创建新文件,并覆盖内容,但是要对原有的文件追加写入就不行了。为了追加写入内容到旧文件中,需要使用os.OpenFile。
根据go 文档,os.OpenFile相对于os.Open和os.Create更通用。因为os.Create和os.Open内部都是调它的。
除了文件路径,os.OpenFile需要flags整数和权限位参数,返回一个File结构体。为了对文件进行读写,需要组合正确的flags来完成:
const (
// 必须指定O_RDONLY, O_WRONLY, or O_RDWR
O_RDONLY int = syscall.O_RDONLY // open the file read-only.
O_WRONLY int = syscall.O_WRONLY // open the file write-only.
O_RDWR int = syscall.O_RDWR // open the file read-write.
// 剩下的使用|逻辑符控制行为
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 //打开文件时截断文件
)
我们可以结合O_APPEND和O_WRONLY,使用|连接符,然后传给os.OpenFile来得到File结构体。如果使用File.Write,数据会追加写入到文件。
import (
"log"
"os"
)
/*
append.txt initally:
An existing line
append.txt after calling appendToFile:
An existing line
Adding a new line
*/
func appendToFile(){
content := "\nAdding a new line";
file, err := os.OpenFile("append.txt", os.O_APPEND | os.O_WRONLY, 0644);
defer file.Close();
if err != nil {
log.Fatal(err);
}
file.Write([]byte(content));
}
删除文件
os.Remove 获取到一个文件或一个空目录的路径并删除该文件/目录。如果文件不存在,将返回一个非nil错误。
import (
"log"
"os
)
func removeFile(){
err := os.Remove("./removeFolder/remove.txt");
if err != nil{
log.Fatal(err);
}
}
现在我们学习了文件的基本操作,下面来研究下目录的使用。
目录
创建目录
要创建一个新的目录,可以使用os.Mkdir。os.Mkdir需要传入目录名称和权限位,然后创建一个新的目录。如果函数创建目录失败会返回错误。
import (
"log"
"os"
)
func makeDir(){
err := os.Mkdir("newDirectory", 0755);
if err != nil {
log.Fatal(err);
}
}
在某些情况下,可能需要仅在程序执行期间存在的临时目录。操作系统。可以使用MkdirTemp创建这样的目录。
import (
"log"
"os"
)
/*
os.MkdirTemp takes in the path to make the temporary dir and a pattern.
os.MkdirTemp will make a new directory with a name which is the pattern + a random string.
Ex: if "transform" was my pattern, a potential temp directory can be:
./temporary/transform952209073
*/
func makeTempDir(){
dirName, err := os.MkdirTemp("./temporary", "transform");
defer os.RemoveAll(dirName); // remove all contents in a directory
if err != nil {
log.Fatal(err);
}
}
os.MkdirTemp确保临时目录创建后有唯一的名称,即使被多个goroutines或程序调用。而且,一旦使用完临时目录,确保删除它使用osRemoveAll删除目录里面的所有内容。
切换目录和读取目录
首先,我们可以使用os.Getwd获取当前工作目录。
import (
"log"
"os"
)
func getWd() {
dir, err = os.Getwd()
if err != nil {
log.Fatal(err);
}
return dir;
}
可以使用os.Chdir来切换目录。
import (
"log"
"os"
)
func navigate(){
os.Getwd() // Working Directory: ./folder
os.Chdir("./item"); // Working Directory: ./folder/item
os.Chdir("../"); // Working Directory: ./folder
}
除了改变工作目录,我们还可以通过os.ReadDir获取目录子目录。os.ReadDir接受一个目录路径并返回一个DirEntry结构的数组,如果不成功则返回一个非空错误。
type DirEntry interface {
// Name returns the name of the file (or subdirectory) described by the entry.
// This name is only the final element of the path (the base name), not the entire path.
// For example, Name would return "hello.go" not "/home/gopher/hello.go".
Name() string
// IsDir reports whether the entry describes a directory.
IsDir() bool
// Type returns the type bits for the entry.
// The type bits are a subset of the usual FileMode bits, those returned by the FileMode.Type method.
Type() FileMode
// Info returns the FileInfo for the file or subdirectory described by the entry.
// The returned FileInfo may be from the time of the original directory read
// or from the time of the call to Info. If the file has been removed or renamed
// since the directory read, Info may return an error satisfying errors.Is(err, ErrNotExist).
// If the entry denotes a symbolic link, Info reports the information about the link itself,
// not the link's target.
Info() (FileInfo, error)
}
下面是它的用法:
import (
"fmt"
"log"
"os"
)
/*
Suppose this was the directory structure of test:
- test
- a.txt
- b
- c.txt
getDirectoryContents will print out "a.txt" and "b".
*/
func getDirectoryContents(){
entries, err := os.ReadDir("./test");
if err != nil {
log.Fatal(err);
}
//iterate through the directory entities and print out their name.
for _, entry := range(entries) {
fmt.Println(entry.Name());
}
}
浏览目
使用os.Chdir和os.ReadDir,我们可以浏览对应目录下的所有的文件和子目录,但path/filePath包提供了一个更优雅的方式,使用filepath.WalkDir来实现文件和目录的浏览。
filepath.WalkDir需要输入待浏览的目录,以及一个回调函数:
type WalkDirFunc func(path string, d DirEntry, err error) error
fn将在传入的目录中的每个文件和子目录上被调用。下面是一个计算dang q目录中所有文件数量的示例。
import (
"fmt"
"io/fs"
"path/filepath"
)
// example of counting all files in a root directory
func countFiles() int {
count := 0;
filepath.WalkDir(".", func(path string, file fs.DirEntry, err error) error {
if err != nil {
return err
}
if !file.IsDir() {
fmt.Println(path);
count++;
}
return nil;
});
return count;
}
path/filepath提供了另一个函数filepath.Walk和filepath.WalkDir功能相同。然而,文档中说明filepath.Walk效率更低。因此使用filepath.WalkDir会更好。