Skip to content

Files

Latest commit

8d041bb · Oct 24, 2021

History

History
739 lines (557 loc) · 31.6 KB

File metadata and controls

739 lines (557 loc) · 31.6 KB

四、使用文件系统

本章介绍与 Unix 文件系统的交互。这里,我们将介绍从基本读写操作到更高级的缓冲操作(如令牌扫描和文件监视)的所有内容。

用户或系统的所有信息都作为文件存储在 Unix 中,因此为了与系统和用户数据交互,我们必须与文件系统交互。

在本章中,我们将看到有不同的执行读写操作的方法,以及每种方法如何更加关注代码的简单性、应用程序的内存使用情况及其性能,以及执行速度。

本章将介绍以下主题:

  • 文件路径操作
  • 读取文件
  • 写文件
  • 其他文件系统操作
  • 第三方软件包

技术要求

本章要求安装 Go 并设置您最喜爱的编辑器。更多信息请参见第三章Go概述。

处理路径

Go 提供了一系列函数,可以操作独立于平台的文件路径,这些路径主要包含在path/filepathos包中。

工作目录

每个进程都有一个与之相关联的目录,称为工作目录,通常从父进程继承。这样就可以指定相对路径,而不是以根文件夹开始的路径。在 Unix 和 macOS 上为/,在 Windows 上为C:\(或任何其他驱动器号)。

绝对/完整路径以根目录开始,表示文件系统中的相同位置,即/usr/local

相对路径不以根开始,路径以当前工作目录开始,即documents

操作系统将这些路径解释为相对于当前目录,因此它们的绝对版本是工作目录和相对路径的串联。让我们来看看下面的例子:

user:~ $ cd documents

user:~/documents $ cd ../videos

user:~/videos $

在此,用户位于其主文件夹~中。用户指定将目录更改为documentscd命令自动添加工作目录作为前缀,并移动到~/documents

在继续第二个命令之前,让我们介绍两个特殊文件,它们位于所有操作系统的所有目录中:

  • .:点是对当前目录的引用。如果它是路径的第一个元素,则它是流程工作目录,否则它指它前面的路径元素(例如在~/./documents中,.~
  • ..:如果是路径的第一个元素,则双点表示当前目录的父目录;如果不是,则双点表示当前目录前面目录的父目录(例如在~/images/../documents中,..表示~/images~的父目录)。

知道了这一点,我们可以很容易地推断第二条路径首先在~/documents/../videos中加入,父元素..得到解析,最终路径~/videos得到。

获取和设置工作目录

我们可以使用os包的func Getwd() (dir string, err error)函数来找出哪个路径代表当前的工作目录。

更改工作目录是使用同一软件包的另一个功能完成的,即func Chdir(dir string) error,如下代码所示:

wd, err := os.Getwd()
if err != nil {
    fmt.Println(err)
    return
}
fmt.Println("starting dir:", wd)

if err := os.Chdir("/"); err != nil {
    fmt.Println(err)
    return
}

if wd, err = os.Getwd(); err != nil {
    fmt.Println(err)
    return
}
fmt.Println("final dir:", wd)

路径操纵

filepath包包含的函数不到 20 个,与标准库的包相比,这是一个很小的数字,它用于操作路径。让我们来看看这些函数:

  • func Abs(path string) (string, error):返回通过将路径加入当前工作目录而传递的路径的绝对版本(如果它不是绝对版本),然后清除它。
  • func Base(path string) string:给出路径的最后一个元素(基)。例如,path/to/some/file返回文件*。*注意,如果路径为空,则此函数返回一个*.*(点)路径。
  • func Clean(path string) string:通过应用一系列定义的规则返回路径的最短版本。它可以执行诸如更换...或拆除拖尾分离器等操作。
  • func Dir(path string) string:获取没有最后一个元素的路径。这通常返回元素的父目录。
  • func EvalSymlinks(path string) (string, error):返回评估符号链接后的路径。如果提供的路径也是相对的,并且不包含具有绝对路径的符号链接,则该路径是相对的。
  • func Ext(path string) string:获取路径的文件扩展名,后缀以路径最后一个元素的最后一个点开始,如果没有点,则为空字符串(例如,docs/file.txt返回.txt
  • func FromSlash(path string) string:用操作系统路径分隔符替换路径中的所有/(斜线)。如果操作系统是 Windows,则此函数不执行任何操作,并在 Unix 或 macOS 下执行替换。
  • func Glob(pattern string) (matches []string, err error):查找与指定模式匹配的所有文件。如果没有匹配的文件,则结果为nil。它不会报告路径探索过程中发生的最终错误。它与Match共享语法。
  • func HasPrefix(p, prefix string) bool:此功能已弃用。
  • func IsAbs(path string) bool:显示路径是否为绝对路径。
  • func Join(elem ...string) string:通过使用文件路径分隔符连接多个路径元素来连接它们。注意,这也会对结果调用Clean
  • func Match(pattern, name string) (matched bool, err error):验证给定名称是否与模式匹配,允许使用野生char字符*?以及使用方括号的字符组或序列。
  • func Rel(basepath, targpath string) (string, error):返回从基准到目标路径的相对路径,如果不可能,则返回错误。此函数对结果调用Clean
  • func Split(path string) (dir, file string):使用最后的尾随斜杠将路径分为两部分。结果通常是父路径和输入路径的文件名。如果没有分隔符,dir将为空,文件将是路径本身。
  • func SplitList(path string) []string:返回路径列表,用列表分隔符分隔,在 Unix 和 macOS 中为:,在 Windows 中为;
  • func ToSlash(path string) string:操作FromSlash函数执行的相反替换,将每个路径分隔符更改为/,在 Unix 和 macOS 上不做任何操作,在 Windows 中执行替换。
  • func VolumeName(path string) string:这在非 Windows 平台上不起任何作用。它返回指向卷的路径组件。这对本地路径和网络资源都是如此。
  • func Walk(root string, walkFn WalkFunc)``error:该函数从根目录开始,递归遍历文件树,对树的每个条目执行 walk 函数。如果 walk 函数返回错误,则 walk 将停止并返回该错误。该函数定义如下:
type WalkFunc func(path string, info os.FileInfo, err error) error

在继续下一个示例之前,我们先介绍一个重要变量:os.Args。此变量至少包含一个值,即调用当前进程的路径。后面可以是在同一调用中指定的最终参数。

我们想实现一个小应用程序,它列出并统计目录中的文件数。我们可以使用刚才看到的一些工具来实现这一点。

以下代码中显示了列表和计数文件的示例:

package main

import (
    "fmt"
    "os"
    "path/filepath"
)

func main() {
    if len(os.Args) != 2 { // ensure path is specified
        fmt.Println("Please specify a path.")
        return
    }
    root, err := filepath.Abs(os.Args[1]) // get absolute path
    if err != nil {
        fmt.Println("Cannot get absolute path:", err)
        return
    }
    fmt.Println("Listing files in", root)
    var c struct {
        files int
        dirs int
    }
    filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
        // walk the tree to count files and folders
        if info.IsDir() {
            c.dirs++
        } else {
            c.files++
        }
        fmt.Println("-", path)
        return nil
    })
    fmt.Printf("Total: %d files in %d directories", c.files, c.dirs)
}

从文件中读取

可以使用io/ioutil包中的辅助功能以及ReadFile功能获取文件内容,该功能可以立即打开、读取和关闭文件。这将使用一个小的缓冲区(512 字节),并将整个内容加载到内存中。如果文件大小非常大、未知,或者文件内容一次只能处理一部分,则这不是一个好主意。

一次从磁盘读取一个大文件意味着将文件的所有内容复制到主内存中,这是一个有限的资源。这可能会导致内存不足以及运行时错误。一次读取一个文件的块可以帮助读取大文件的内容,而不会造成巨大的内存使用。这是因为在读取下一个块时,内存的相同部分将被重用。

以下代码显示了一次读取所有内容的示例:

package main

import (
    "fmt"
    "io/ioutil"
    "os"
)

func main() {
    if len(os.Args) != 2 {
        fmt.Println("Please specify a path.")
        return
    }
    b, err := ioutil.ReadFile(os.Args[1])
    if err != nil {
        fmt.Println("Error:", err)
    }
    fmt.Println(string(b))
}

读卡器接口

对于从磁盘读取的所有操作,有一个最重要的接口:

type Reader interface {
    Read(p []byte) (n int, err error)
}

它的工作非常简单——用已读取的内容填充给定的字节片,并返回已读取的字节数和错误(如果出现)。有一个由io包定义的特殊错误变量,称为EOF文件结尾),当没有更多可用输入时应返回该变量。

读卡器可以分块处理数据(大小由片决定),如果相同的片被重复用于后续的操作,则生成的程序的内存效率会一直更高,因为它使用的是分配片的内存的相同有限部分。

文件结构

os.File类型满足读者界面,是用于与文件内容交互的主要参与者。获取实例以供阅读的最常见方法是使用os.Open函数。记住在使用完文件后关闭文件是非常重要的——这对于短命程序来说并不明显,但如果应用程序一直在打开文件而没有关闭它所使用的文件,那么应用程序将达到操作系统规定的打开文件的限值,并开始失败打开操作。

shell 提供了几个实用程序,如下所示:

  • 一个用于获取打开文件的限制–ulimit -n
  • 另一种方法是检查某个进程打开了多少文件–lsof -p PID

前面的示例打开一个文件只是为了向标准输出显示其内容,它通过将其所有内容加载到内存中来实现。这可以通过我们刚才提到的工具轻松优化。在下面的示例中,我们使用一个小缓冲区并在下一次读取覆盖它之前打印它的内容,使用一个小缓冲区将内存使用保持在最低限度。

以下代码中显示了使用字节数组作为缓冲区的示例:

func main() {
    if len(os.Args) != 2 {
        fmt.Println("Please specify a file")
        return
    }
    f, err := os.Open(os.Args[1])
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    defer f.Close() // we ensure close to avoid leaks

    var (
        b = make([]byte, 16)
    )
    for n := 0; err == nil; {
        n, err = f.Read(b)
        if err == nil {
            fmt.Print(string(b[:n])) // only print what's been read
        }
    }
    if err != nil && err != io.EOF { // we expect an EOF
        fmt.Println("\n\nError:", err)
    }
}

如果一切正常,读取循环将继续执行读取操作,直到文件内容结束。在这种情况下,读取循环将返回一个io.EOF错误,这表明没有更多可用内容。

使用缓冲区

数据缓冲区或只是一个缓冲区,是内存的一部分,用于在移动时存储临时数据。字节缓冲区是在bytes包中实现的,它们由一个底层片实现,该片能够在每次需要存储的数据量不合适时不断增长。

如果每次分配新的缓冲区,旧的缓冲区最终将由 GC 本身清理,这不是最佳解决方案。最好是重用缓冲区,而不是分配新的缓冲区。这是因为它们可以在保持容量不变的情况下重置切片(GC 不会清除或收集阵列)。

缓冲区还提供两个函数来显示其基本长度和容量。在下面的示例中,我们可以看到如何使用Buffer.Reset重用缓冲区,以及如何跟踪其容量。

以下代码显示了缓冲区重用及其底层容量的示例:

package main

import (
    "bytes"
    "fmt"
)

func main() {
    var b = bytes.NewBuffer(make([]byte, 26))
    var texts = []string{
        `As he came into the window`,
        `It was the sound of a crescendo
He came into her apartment`,
        `He left the bloodstains on the carpet`,
        `She ran underneath the table
He could see she was unable
So she ran into the bedroom
She was struck down, it was her doom`,
    }
    for i := range texts {
        b.Reset()
        b.WriteString(texts[i])
        fmt.Println("Length:", b.Len(), "\tCapacity:", b.Cap())
    }
}

窥视内容

在上一个示例中,我们固定了字节数,以便在每次读取内容时在打印之前存储内容。bufio软件包提供了一些功能,可以使用不受用户直接控制的底层缓冲区,并可以执行名为peek的非常重要的操作。

偷看是在不推进阅读器光标的情况下阅读内容的能力。这里,在引擎盖下,偷看的数据存储在缓冲区中。每次读取操作都会检查此缓冲区中是否有数据,如果有,则会在从缓冲区中删除数据时返回该数据。这就像一个队列(先进先出)。

这个简单的操作打开的可能性是无限的,它们都来自于偷看,直到找到所需的数据序列,然后感兴趣的块被实际读取。此操作最常见的用途包括:

  • 缓冲区一直从读取器读取,直到找到换行符(一次读取一行)。
  • 在找到空格(一次读取一个单词)之前,将使用相同的操作。

允许应用程序实现此行为的结构是bufio.Scanner。这样就可以定义什么是拆分函数,并具有以下类型:

type SplitFunc func(data []byte, atEOF bool) (advance int, token []byte, err error)

当返回错误时,此函数将停止,否则它将返回要在内容中前进的字节数,并最终返回令牌。包中实现的功能如下:

  • ScanBytes:字节令牌
  • ScanRunes:符文代币
  • ScanWord:文字标记
  • ScanLines:行令牌

我们可以实现一个文件读取器,只需一个读取器就可以计算行数。生成的程序将尝试模拟 Unixwc -l命令的功能。

以下代码显示了打印文件和计数行的示例:

func main() {
    if len(os.Args) != 2 {
        fmt.Println("Please specify a path.")
        return
    }
    f, err := os.Open(os.Args[1])
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    defer f.Close()
    r := bufio.NewReader(f) // wrapping the reader with a buffered one
    var rowCount int
    for err == nil {
        var b []byte
        for moar := true; err == nil && moar; {
            b, moar, err = r.ReadLine()
            if err == nil {
                fmt.Print(string(b))
            }
        }
        // each time moar is false, a line is completely read
        if err == nil {
            fmt.Println()
            rowCount++

        }
    }
    if err != nil && err != io.EOF {
        fmt.Println("\nError:", err)
        return
    }
    fmt.Println("\nRow count:", rowCount)
}

近距离搜索器

还有另外两个与读卡器相关的接口:io.Closerio.Seeker

type Closer interface {
        Close() error
}

type Seeker interface {
        Seek(offset int64, whence int) (int64, error)
}

这些通常与io.Reader组合,产生的接口如下:

type ReadCloser interface {
        Reader
        Closer
}

type ReadSeeker interface {
        Reader
        Seeker
}

Close方法可确保资源得到释放并避免泄漏,而Seek方法可将当前对象(例如,Writer的光标从文件的开始/结束或其当前位置移动到所需的偏移量。

os.File结构实现了该方法,因此它满足所有列出的接口。操作结束时,可以关闭文件,也可以移动当前光标,具体取决于您试图实现的目标。

写入文件

正如我们在阅读中所看到的,有不同的方法来编写文件,每种方法都有自己的缺点和优点。例如,在ioutil包中,我们有另一个名为WriteFile的函数,它允许我们在一行中执行整个操作。这包括打开文件、写入文件内容,然后关闭文件。

以下代码显示了一次写入所有文件内容的示例:

package main

import (
    "fmt"
    "io/ioutil"
    "os"
)

func main() {
    if len(os.Args) != 3 {
        fmt.Println("Please specify a path and some content")
        return
    }
    // the second argument, the content, needs to be casted to a byte slice
    if err := ioutil.WriteFile(os.Args[1], []byte(os.Args[2]), 0644); err != nil {
        fmt.Println("Error:", err)
    }
}

本例在一次操作中一次性写入所有内容。这要求我们使用字节片分配内存中的所有内容。如果内容太大,内存使用可能会成为操作系统的一个问题,这可能会扼杀应用程序的进程。

如果内容的大小不是很大,并且应用程序的寿命很短,那么,如果内容加载到内存中并通过单个操作写入,则不是问题。这并不是长寿命应用程序的最佳实践,这些应用程序正在对许多不同的文件执行读写操作。他们必须分配内存中的所有内容,并且该内存将在某个时候由 GC 释放–此操作不是免费的,这意味着它在内存使用和性能方面存在缺点。

编写器接口

对于阅读有效的原则同样适用于写作,io包中有一个确定写作行为的接口,如以下代码所示:

type Writer interface {
        Write(p []byte) (n int, err error)
}

io.Writer接口定义了一个方法,给定一个字节片,该方法返回其中有多少字节被写入和/或是否有任何错误。writer 可以一次写入一个数据块,而不需要同时拥有所有数据块。os.File结构恰好也是一个 writer,可以以这种方式使用。

我们可以使用一段字节作为缓冲区来逐段写入信息。在下面的示例中,我们将尝试将上一节的阅读与写作结合起来,使用io.Seeker功能在写作之前反转其内容。

以下代码显示了反转文件内容的示例:

// Let's omit argument check and file opening, we obtain src and dst
cur, err := src.Seek(0, os.SEEK_END) // Let's go to the end of the file
if err != nil {
    fmt.Println("Error:", err)
    return
}
b := make([]byte, 16)

移动到文件末尾并定义字节缓冲区后,我们在文件中输入一个稍微向后的循环,然后读取其中的一部分,如以下代码所示:

for step, r, w := int64(16), 0, 0; cur != 0; {
    if cur < step { // ensure cursor is 0 at max
        b, step = b[:cur], cur
    }
    cur = cur - step
    _, err = src.Seek(cur, os.SEEK_SET) // go backwards
    if err != nil {
        break
    }
    if r, err = src.Read(b); err != nil || r != len(b) {
        if err == nil { // all buffer should be read
            err = fmt.Errorf("read: expected %d bytes, got %d", len(b), r)
        }
        break
    }

然后,我们反转内容并将其写入目标,如以下代码所示:

    for i, j := 0, len(b)-1; i < j; i, j = i+1, j-1 {
        switch { // Swap (\r\n) so they get back in place
        case b[i] == '\r' && b[i+1] == '\n':
            b[i], b[i+1] = b[i+1], b[i]
        case j != len(b)-1 && b[j-1] == '\r' && b[j] == '\n':
            b[j], b[j-1] = b[j-1], b[j]
        }
        b[i], b[j] = b[j], b[i] // swap bytes
    }
    if w, err = dst.Write(b); err != nil || w != len(b) {
        if err != nil {
            err = fmt.Errorf("write: expected %d bytes, got %d", len(b), w)
        }
    }
}
if err != nil && err != io.EOF { // we expect an EOF
    fmt.Println("\n\nError:", err)
}

缓冲区和格式

在上一节中,我们看到了如何使用bytes.Buffer临时存储数据,以及它如何通过附加底层切片来处理自身的增长。fmt包广泛使用缓冲区执行其操作;由于依赖性原因,这些不是字节包中的字节。这种方法是 Go 的一句谚语所固有的:

“一点拷贝比一点依赖要好。”

如果必须导入一个包来使用一个函数或类型,则应该考虑只将必要的代码复制到自己的包中。如果一个包包含的内容远远超过您需要的内容,那么通过复制可以减少二进制文件的最终大小。您还可以自定义代码并根据自己的需要进行定制。

缓冲区的另一个用途是在写入消息之前编写消息。让我们编写一些代码,以便使用缓冲区格式化书籍列表:

const grr = "G.R.R. Martin"

type book struct {
    Author, Title string
    Year int
}

func main() {
    dst, err := os.OpenFile("book_list.txt", os.O_CREATE|os.O_WRONLY, 0666)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    defer dst.Close()
    bookList := []book{
        {Author: grr, Title: "A Game of Thrones", Year: 1996},
        {Author: grr, Title: "A Clash of Kings", Year: 1998},
        {Author: grr, Title: "A Storm of Swords", Year: 2000},
        {Author: grr, Title: "A Feast for Crows", Year: 2005},
        {Author: grr, Title: "A Dance with Dragons", Year: 2011},
        // if year is omitted it defaulting to zero value
        {Author: grr, Title: "The Winds of Winter"},
        {Author: grr, Title: "A Dream of Spring"},
    }
    b := bytes.NewBuffer(make([]byte, 0, 16))
    for _, v := range bookList {
        // prints a msg formatted with arguments to writer
        fmt.Fprintf(b, "%s - %s", v.Title, v.Author)
        if v.Year > 0 { 
            // we do not print the year if it's not there
            fmt.Fprintf(b, " (%d)", v.Year)
        }
        b.WriteRune('\n')
        if _, err := b.WriteTo(dst); true { // copies bytes, drains buffer
            fmt.Println("Error:", err)
            return
        }
    }
}

缓冲区用于编写图书描述,如果不存在年份,则省略年份。这在处理字节时非常有效,如果每次都重用缓冲区,效果会更好。如果这种操作的输出应该是一个字符串,那么在名为Builderstrings包中有一个非常类似的结构,它具有相同的写入方法,但有一些不同,例如:

  • String()方法使用unsafe包将字节转换为字符串,而不是复制它们。
  • 不允许复制strings.Builder然后写入副本,因为这会导致panic

高效写作

每次执行os.File方法,即Write,都会转换为一个系统调用,这是一个带有一些开销的操作。一般来说,通过一次写入更多数据来减少操作次数是一个好主意,从而减少此类调用所花费的时间。

bufio.Writer结构是一个 writer,它包装另一个 writer,如os.File,并且仅在缓冲区已满时执行写操作。这使得可以使用Flush方法执行强制写入,该方法通常保留到写入过程结束。使用缓冲区的良好模式如下:

  var w io.WriteCloser
  // initialise writer
  defer w.Close()
  b := bufio.NewWriter(w)
  defer b.Flush()
  // write operations

defer语句在返回当前函数之前按相反顺序执行,因此第一个Flush确保写入缓冲区中仍然存在的内容,然后Close实际关闭文件。如果这两个操作以相反的顺序执行,flush 将尝试写入一个关闭的文件,返回一个错误,并且无法写入最后一块信息。

文件模式

我们看到,os.OpenFile函数可以选择如何以文件模式打开文件,这是一个uint32,其中每个位都有一个含义(如 Unix 文件和文件夹权限)。os包提供了一系列值,每个值指定一种模式,正确的组合方式是使用|(按位或)。

以下代码显示了可用的代码,这些代码直接取自 Go 的源代码:

// Exactly one of O_RDONLY, O_WRONLY, or O_RDWR must be specified.
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.
// The remaining values may be or'ed in to control behavior.
O_APPEND int = syscall.O_APPEND // append data to the file when writing.
O_CREATE int = syscall.O_CREAT // create a new file if none exists.
O_EXCL int = syscall.O_EXCL // used with O_CREATE, file must not exist.
O_SYNC int = syscall.O_SYNC // open for synchronous I/O.
O_TRUNC int = syscall.O_TRUNC // if possible, truncate file when opened.

前三个表示允许的操作(读、写或两者兼有),其他操作如下:

  • O_APPEND:每次写入前,文件偏移量位于文件末尾。
  • O_CREATE:如果文件不存在,则可以创建该文件。
  • O_EXCL:如果与创建一起使用,则如果文件已经存在(独占创建),则失败。
  • O_SYNC:执行读写操作并验证其竞争性。
  • O_TRUNC:如果文件存在,则其大小被截断为0

其他业务

读和写不是可以在文件上执行的唯一操作。在下一节中,我们将了解如何使用os包使用它们。

创造

为了创建一个空文件,我们可以调用一个名为Create的助手函数,该函数使用0666权限打开一个新文件,如果它不存在,则将其截断。或者,我们可以使用OpenFileO_CREATE|O_TRUNCATE模式来指定自定义权限,如下代码所示:

package main

import "os"

func main() {
    f, err := os.Create("file.txt")
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    f.Close()
}

截断

要在某个维度下截断文件的内容,如果文件较小,则保持文件不变,有os.Truncate方法。它的用法非常简单,如下代码所示:

package main

import "os"

func main() {
    // let's keep thing under 4kB
    if err := os.Truncate("file.txt", 4096); err != nil {
        fmt.Println("Error:", err)
    }
}

删去

为了删除一个文件,还有一个简单的功能,叫做os.Remove,如下代码所示:

package main

import "os"

func main() {
    if err := os.Remove("file.txt"); err != nil {
        fmt.Println("Error:", err)
    }
}

移动

os.Rename功能可以更改文件名和/或其目录。请注意,如果目标文件已存在,则此操作将替换该文件。

更改文件名或其目录的代码如下:

import "os"

func main() {
    if err := os.Rename("file.txt", "../file.txt"); err != nil {
        fmt.Println("Error:", err)
    }
}

复制

没有独特的功能可以复制文件,但这可以通过具有io.Copy功能的读写器轻松完成。以下示例显示了如何使用它从一个文件复制到另一个文件:

func CopyFile(from, to string) (int64, error) {
    src, err := os.Open(from)
    if err != nil {
        return 0, err
    }
    defer src.Close()
    dst, err := os.OpenFile(to, os.O_WRONLY|os.O_CREATE, 0644)
    if err != nil {
        return 0, err
    }
    defer dst.Close()  
    return io.Copy(dst, src)
}

统计数据

os包提供FileInfo接口,返回文件元数据,如下代码所示:

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() interface{} // underlying data source (can return nil)
}

os.Stat函数返回指定路径的文件信息。

改变属性

为了与文件系统交互并更改这些属性,有三个功能可用:

  • func Chmod(name string, mode FileMode) error:更改文件的权限
  • func Chown(name string, uid, gid int) error:更改文件的所有者和组
  • func Chtimes(name string, atime time.Time, mtime time.Time) error:更改文件的访问和修改时间

第三方软件包

社区提供了许多包来完成各种任务。在本节中,我们将快速查看其中一些。

虚拟文件系统

文件是 Go 中的一个结构,是一种具体类型,它们周围没有抽象,而文件的信息由os.FileInfo表示,它是一个接口。这有点不一致,有很多人试图在文件系统上创建一个完整且一致的抽象,通常称为虚拟文件系统

最常用的两个软件包如下:

即使它们是单独开发的,它们都做同样的事情——它们使用os.File的所有方法定义一个接口,然后定义一个实现os包中可用功能的接口,如创建、打开和删除文件等。

他们提供了一个基于os.File的版本,该版本使用标准包实现,但也有一个内存版本使用模拟文件系统的数据结构。这对于为任何包构建测试都非常有用。

文件系统事件

Go 在位于 Go 的 GitHub 处理程序(下的golang.org/x/包中有一些实验特性 https://github.com/golang/ )。golang.org/x/sys包是此列表的一部分,包括一个子包,专门用于 Unix 系统事件。这已经被用于构建 Go 文件功能中缺少的功能,并且非常有用——观察文件上事件(如创建、删除和更新)的特定路径。

两个最著名的实现如下所示:

这两个包都公开了一个允许创建观察者的函数。监视程序是包含负责传递文件事件的通道的结构。它们还公开了另一个函数,该函数负责终止/关闭监视程序和底层通道。

总结

在本章中,我们概述了如何在 Go 中执行文件操作。为了定位文件,filepath包提供了大量函数。这些可以帮助您执行所有类型的操作,从组合路径到从中提取元素。

我们还研究了如何使用各种方法读取操作,从io/ioutil包中最简单、内存效率较低的方法,到需要io.Writer实现来读取固定字节块的方法。在bufio包中实现的窥视内容的能力的重要性,允许进行一整套操作,如读字或读行,当找到令牌时停止读取操作。还有一些非常有用的文件满足的其他接口;例如,io.Closer确保资源被释放,io.Seeker用于在不需要实际读取文件和放弃输出的情况下移动读取光标。

将字节片写入文件可以通过不同的方式实现–使用io/ioutil包可以通过函数调用实现,而对于更复杂或更节省内存的操作,则可以使用io.Writer接口。这使得一次可以写入一个字节片,fmt包可以使用它来打印格式化数据。缓冲写入用于减少磁盘上的实际写入量。这是通过一个缓冲区来完成的,该缓冲区收集内容,然后在每次内容满时将其传输到磁盘。

最后,我们了解了如何在文件系统上完成其他文件操作(创建、删除、复制/移动和更改文件属性),并了解了一些与文件系统相关的第三方包,即虚拟文件系统抽象和文件系统事件通知。

下一章将讨论流,并将重点讨论与文件系统无关的所有读写器实例。

问题

  1. 绝对路径和相对路径之间有什么区别?
  2. 如何获取或更改当前工作目录?
  3. 使用ioutil.ReadAll的优点和缺点是什么?
  4. 为什么缓冲区对于读取操作很重要?
  5. 您应该何时使用ioutil.WriteFile
  6. 使用允许窥视的缓冲读取器时,哪些操作可用?
  7. 什么时候使用字节缓冲区读取内容更好?
  8. 如何使用缓冲区进行写入?使用它们有什么好处?