Go:文件系统IO

时间:2021-7-3 作者:qvyue

Go:文件系统IO

原文地址

摘要

在任何语言中,读写磁盘和浏览文件系统都是重点内容。让我们使用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会更好。

声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:qvyue@qq.com 进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。