Skip to content

Latest commit

 

History

History
1274 lines (1020 loc) · 40.9 KB

03.md

File metadata and controls

1274 lines (1020 loc) · 40.9 KB

三、使用文件

Unix 和 Linux 系统的定义特性之一是如何将所有内容作为文件处理。进程、文件、目录、套接字、设备和管道都被视为文件。鉴于操作系统的这一基本特性,学习如何操作文件是一项关键技能。本章提供了几个操作文件的不同方法的示例。

首先,我们将了解基本知识,即创建、截断、删除、打开、关闭、重命名和移动文件。我们还将了解如何获取文件的详细属性,例如权限和所有权、大小和符号链接信息。

本章有一整节专门介绍读取和写入文件的不同方式。有多个包含有用函数的包;此外,读写器接口支持许多不同的选项,例如缓冲读写器、直接读写、扫描仪和用于快速操作的辅助功能。

此外,还提供了存档和取消存档、压缩和解压缩、创建临时文件和目录以及通过 HTTP 下载文件的示例。

具体而言,本章将涵盖以下主题:

  • 创建空文件和截断文件
  • Getting detailed file information
  • Renaming, moving, and deleting files
  • Manipulating permissions, ownership, and timestamps
  • 符号链接
  • 读取和写入文件的多种方式
  • 档案
  • 压缩
  • 临时文件和目录
  • 通过 HTTP 下载文件

File basics

由于文件是计算生态系统中不可或缺的一部分,因此了解 Go 中使用文件的可用选项至关重要。本节介绍一些基本操作,如打开、关闭、创建和删除文件。此外,它还包括重命名和移动、查看文件是否存在、修改权限、所有权、时间戳以及使用符号链接。大多数示例使用硬编码文件名test.txt。如果要对其他文件进行操作,请更改此文件名。

创建空文件

Linux 中使用的一个常用工具是touch程序。当您需要快速创建具有特定名称的空文件时,经常使用它。以下示例复制了创建空文件的常见触摸用例之一。

创建一个空文件的用途有限,但让我们考虑一个例子。如果有一个服务将日志写入一组旋转的文件,该怎么办。每天使用当前日期创建一个新文件,并将当天的日志写入该文件。开发人员可能很聪明,对日志文件设置了非常严格的权限,这样只有管理员才能读取它们。但是,如果他们在目录上留下了松散的权限怎么办?如果创建了一个带有第二天日期的空文件,会发生什么情况?只有当日志文件不存在时,服务才能创建一个新的日志文件,但如果确实存在日志文件,则服务将在不检查权限的情况下使用它。您可以通过创建一个具有读取权限的空文件来利用这一点。文件的命名方式应与服务命名日志文件的方式相同。例如,如果该服务对日志使用如下格式:logs-2018-01-30.txt,您可以创建一个名为logs-2018-01-31.txt的空文件,第二天,该服务将写入该文件,因为该文件已经存在,您将拥有读取权限,而不是在不存在文件的情况下,该服务创建一个仅具有 root 权限的新文件。

以下是此示例的代码实现:

package main 

import ( 
   "log" 
   "os" 
) 

func main() { 
   newFile, err := os.Create("test.txt") 
   if err != nil { 
      log.Fatal(err) 
   } 
   log.Println(newFile) 
   newFile.Close() 
} 

截断文件

截断文件是指将文件修剪到最大长度。截断通常用于完全删除文件的所有内容,但也可用于将文件限制为特定的最大大小。os.Truncate()的一个显著特征是,如果文件小于指定的截断限制,它实际上会增加文件的长度。它将用空字节填充任何空白。

截断文件比创建空文件有更多的实际用途。当日志文件太大时,可以将其截断以节省磁盘空间。如果您正在攻击,您可能需要截断.bash_history和其他日志文件以覆盖您的轨迹。实际上,恶意参与者可能只是为了破坏数据而截断文件。

package main 

import ( 
   "log" 
   "os" 
) 

func main() { 
   // Truncate a file to 100 bytes. If file 
   // is less than 100 bytes the original contents will remain 
   // at the beginning, and the rest of the space is 
   // filled will null bytes. If it is over 100 bytes, 
   // Everything past 100 bytes will be lost. Either way 
   // we will end up with exactly 100 bytes. 
   // Pass in 0 to truncate to a completely empty file 

   err := os.Truncate("test.txt", 100) 
   if err != nil { 
      log.Fatal(err) 
   } 
} 

获取文件信息

下面的示例将打印出有关文件的所有可用元数据。它包括明显的属性,即名称、大小、权限、上次修改时间以及是否为目录。它包含的最后一个数据块是FileInfo.Sys()接口。其中包含有关文件的底层源的信息,该文件通常是硬盘上的文件系统:

package main 

import ( 
   "fmt" 
   "log" 
   "os" 
) 

func main() { 
   // Stat returns file info. It will return 
   // an error if there is no file. 
   fileInfo, err := os.Stat("test.txt") 
   if err != nil { 
      log.Fatal(err) 
   } 
   fmt.Println("File name:", fileInfo.Name()) 
   fmt.Println("Size in bytes:", fileInfo.Size()) 
   fmt.Println("Permissions:", fileInfo.Mode()) 
   fmt.Println("Last modified:", fileInfo.ModTime()) 
   fmt.Println("Is Directory: ", fileInfo.IsDir()) 
   fmt.Printf("System interface type: %T\n", fileInfo.Sys()) 
   fmt.Printf("System info: %+v\n\n", fileInfo.Sys()) 
} 

重命名文件

标准库提供了移动文件的方便功能。重命名和移动是同义词;如果要将文件从一个目录移动到另一个目录,请使用os.Rename()功能,如下代码块所示:

package main 

import ( 
   "log" 
   "os" 
) 

func main() { 
   originalPath := "test.txt" 
   newPath := "test2.txt" 
   err := os.Rename(originalPath, newPath) 
   if err != nil { 
      log.Fatal(err) 
   } 
} 

删除文件

下面的示例非常简单,它演示了如何删除文件。标准包提供了os.Remove(),需要一个文件路径:

package main 

import ( 
   "log" 
   "os" 
) 

func main() { 
   err := os.Remove("test.txt") 
   if err != nil { 
      log.Fatal(err) 
   } 
} 

打开和关闭文件

打开文件时,有几个选项。调用os.Open()时,只需要文件名,并提供只读文件。另一个选项是使用os.OpenFile(),这需要更多的选项。您可以指定是要只读文件还是只写文件。您还可以选择读写、追加、创建(如果不存在)或在打开时截断。传递与逻辑 OR 运算符组合的所需选项。关闭文件是通过调用文件对象上的Close()来完成的。您可以显式关闭文件,也可以推迟调用。有关defer关键字的更多详细信息,请参阅第 2 章Go 编程语言。以下示例未使用defer关键字选项,但后面的示例将:

package main 

import ( 
   "log" 
   "os" 
) 

func main() { 
   // Simple read only open. We will cover actually reading 
   // and writing to files in examples further down the page 
   file, err := os.Open("test.txt") 
   if err != nil { 
      log.Fatal(err) 
   }  
   file.Close() 

   // OpenFile with more options. Last param is the permission mode 
   // Second param is the attributes when opening 
   file, err = os.OpenFile("test.txt", os.O_APPEND, 0666) 
   if err != nil { 
      log.Fatal(err) 
   } 
   file.Close() 

   // Use these attributes individually or combined 
   // with an OR for second arg of OpenFile() 
   // e.g. os.O_CREATE|os.O_APPEND 
   // or os.O_CREATE|os.O_TRUNC|os.O_WRONLY 

   // os.O_RDONLY // Read only 
   // os.O_WRONLY // Write only 
   // os.O_RDWR // Read and write 
   // os.O_APPEND // Append to end of file 
   // os.O_CREATE // Create is none exist 
   // os.O_TRUNC // Truncate file when opening 
} 

检查文件是否存在

检查文件是否存在需要两个步骤。首先,必须对文件调用os.Stat()才能获取FileInfo。如果文件不存在,则不返回FileInfo结构,但返回错误。os.Stat()可能返回多个错误,因此必须检查错误类型。标准库提供了一个名为os.IsNotExist()的函数,该函数将检查错误是否是由于文件不存在而导致的。

如果文件不存在,下面的示例将调用log.Fatal(),但是如果需要,您可以优雅地处理错误并继续,而不必退出:

package main 

import ( 
   "log" 
   "os" 
) 

func main() { 
   // Stat returns file info. It will return 
   // an error if there is no file. 
   fileInfo, err := os.Stat("test.txt") 
   if err != nil { 
      if os.IsNotExist(err) { 
         log.Fatal("File does not exist.") 
      } 
   } 
   log.Println("File does exist. File information:") 
   log.Println(fileInfo) 
} 

检查读写权限

与前面的示例类似,通过使用名为os.IsPermission()的函数检查错误来检查读写权限。如果传递的错误是由于权限问题引起的,则此函数将返回 true,如下例所示:

package main 

import ( 
   "log" 
   "os" 
) 

func main() { 
   // Test write permissions. It is possible the file 
   // does not exist and that will return a different 
   // error that can be checked with os.IsNotExist(err) 
   file, err := os.OpenFile("test.txt", os.O_WRONLY, 0666) 
   if err != nil { 
      if os.IsPermission(err) { 
         log.Println("Error: Write permission denied.") 
      } 
   } 
   file.Close() 

   // Test read permissions 
   file, err = os.OpenFile("test.txt", os.O_RDONLY, 0666) 
   if err != nil { 
      if os.IsPermission(err) { 
         log.Println("Error: Read permission denied.") 
      } 
   } 
   file.Close()
} 

更改权限、所有权和时间戳

如果您拥有文件或具有适当的权限,则可以更改所有权、时间戳和权限。标准库提供了一组函数。它们在这里给出:

  • os.Chmod()
  • os.Chown()
  • os.Chtimes()

以下示例演示如何使用这些函数更改文件的元数据:

package main 

import ( 
   "log" 
   "os" 
   "time" 
) 

func main() { 
   // Change permissions using Linux style 
   err := os.Chmod("test.txt", 0777) 
   if err != nil { 
      log.Println(err) 
   } 

   // Change ownership 
   err = os.Chown("test.txt", os.Getuid(), os.Getgid()) 
   if err != nil { 
      log.Println(err) 
   } 

   // Change timestamps 
   twoDaysFromNow := time.Now().Add(48 * time.Hour) 
   lastAccessTime := twoDaysFromNow 
   lastModifyTime := twoDaysFromNow 
   err = os.Chtimes("test.txt", lastAccessTime, lastModifyTime) 
   if err != nil { 
      log.Println(err) 
   } 
} 

硬链接和符号链接

典型的文件只是指向硬盘上某个位置的指针,称为索引节点。硬链接将创建指向同一位置的新指针。只有在删除指向该文件的所有链接后,才会从磁盘中删除该文件。硬链接只能在同一文件系统上工作。硬链接是你可能认为的“正常”链接。

A symbolic link, or soft link, is a little different, it does not point directly to a place on the disk. Symlinks only reference other files by name. They can point to files on different filesystems. However, not all systems support symlinks.

Windows 以前对符号链接没有很好的支持,但是这些示例在 Windows 10 Pro 中进行了测试,如果您具有管理员权限,硬链接和符号链接都可以正常工作。要以管理员身份从命令行执行 Go 程序,请首先以管理员身份打开命令提示符,方法是右键单击它并选择以管理员身份运行。从那里您可以执行程序,符号链接和硬链接将按预期工作。

以下示例演示如何创建硬链接和符号链接文件,以及如何确定文件是否为符号链接,以及如何在不更改原始文件的情况下修改符号链接文件元数据:

package main 

import ( 
   "fmt" 
   "log" 
   "os" 
) 

func main() { 
   // Create a hard link 
   // You will have two file names that point to the same contents 
   // Changing the contents of one will change the other 
   // Deleting/renaming one will not affect the other 
   err := os.Link("original.txt", "original_also.txt") 
   if err != nil { 
      log.Fatal(err) 
   } 

   fmt.Println("Creating symlink") 
   // Create a symlink 
   err = os.Symlink("original.txt", "original_sym.txt") 
   if err != nil { 
      log.Fatal(err) 
   } 

   // Lstat will return file info, but if it is actually 
   // a symlink, it will return info about the symlink. 
   // It will not follow the link and give information 
   // about the real file 
   // Symlinks do not work in Windows 
   fileInfo, err := os.Lstat("original_sym.txt") 
   if err != nil { 
      log.Fatal(err) 
   } 
   fmt.Printf("Link info: %+v", fileInfo) 

   // Change ownership of a symlink only 
   // and not the file it points to 
   err = os.Lchown("original_sym.txt", os.Getuid(), os.Getgid()) 
   if err != nil { 
      log.Fatal(err) 
   } 
} 

读写

读取和写入文件有多种方式。Go 提供的接口使您可以轻松编写自己的函数,这些函数可以与文件或任何其他读写器接口一起工作。

osioioutil软件包之间,您可以找到适合您需要的功能。这些示例涵盖了许多可用的选项。

复制文件

以下示例使用io.Copy()功能将内容从一个读卡器复制到另一个写卡器:

package main 

import ( 
   "io" 
   "log" 
   "os" 
) 

func main() { 
   // Open original file 
   originalFile, err := os.Open("test.txt") 
   if err != nil { 
      log.Fatal(err) 
   } 
   defer originalFile.Close() 

   // Create new file 
   newFile, err := os.Create("test_copy.txt") 
   if err != nil { 
      log.Fatal(err) 
   } 
   defer newFile.Close() 

   // Copy the bytes to destination from source 
   bytesWritten, err := io.Copy(newFile, originalFile) 
   if err != nil { 
      log.Fatal(err) 
   } 
   log.Printf("Copied %d bytes.", bytesWritten) 

   // Commit the file contents 
   // Flushes memory to disk 
   err = newFile.Sync() 
   if err != nil { 
      log.Fatal(err) 
   }  
} 

Seeking positions in a file

The Seek() function is useful for setting the file cursor in a specific location. By default, it starts at offset 0 and moves forward as you read bytes. You might want to reset the cursor back to the beginning of a file or jump directly to a specific location. The Seek() function allows you to do this.

Seek()取两个参数。第一个是距离;您希望以字节为单位移动光标。它可以使用正整数向前移动,如果提供了负数,则可以在文件中向后移动。第一个参数“距离”是相对值,而不是文件中的绝对位置。第二个参数指定相对点的起始位置,称为whencewhence参数是相对偏移的参考点。它可以是012,分别表示文件的开头、当前位置和结尾。

例如,如果指定了Seek(-1, 2),它会将文件游标设置为从文件末尾向后一个字节。Seek(2, 0)将查找file.Seek(5, 1)开头的第二个字节,这将使光标从其当前位置向前移动 5 个字节:

package main 

import ( 
   "fmt" 
   "log" 
   "os" 
) 

func main() { 
   file, _ := os.Open("test.txt") 
   defer file.Close() 

   // Offset is how many bytes to move 
   // Offset can be positive or negative 
   var offset int64 = 5 

   // Whence is the point of reference for offset 
   // 0 = Beginning of file 
   // 1 = Current position 
   // 2 = End of file 
   var whence int = 0 
   newPosition, err := file.Seek(offset, whence) 
   if err != nil { 
      log.Fatal(err) 
   } 
   fmt.Println("Just moved to 5:", newPosition) 

   // Go back 2 bytes from current position 
   newPosition, err = file.Seek(-2, 1) 
   if err != nil { 
      log.Fatal(err) 
   } 
   fmt.Println("Just moved back two:", newPosition) 

   // Find the current position by getting the 
   // return value from Seek after moving 0 bytes 
   currentPosition, err := file.Seek(0, 1) 
   fmt.Println("Current position:", currentPosition) 

   // Go to beginning of file 
   newPosition, err = file.Seek(0, 0) 
   if err != nil { 
      log.Fatal(err) 
   } 
   fmt.Println("Position after seeking 0,0:", newPosition) 
} 

将字节写入文件

您可以只使用os包进行编写,打开文件时已经需要该包。由于所有 Go 可执行文件都是静态链接的二进制文件,因此导入的每个包都会增加可执行文件的大小。其他软件包,如ioioutilbufio提供了更多帮助,但它们不是必需的:

package main 

import ( 
   "log" 
   "os" 
) 

func main() { 
   // Open a new file for writing only 
   file, err := os.OpenFile( 
      "test.txt", 
      os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 
      0666, 
   ) 
   if err != nil { 
      log.Fatal(err) 
   } 
   defer file.Close() 

   // Write bytes to file 
   byteSlice := []byte("Bytes!\n") 
   bytesWritten, err := file.Write(byteSlice) 
   if err != nil { 
      log.Fatal(err) 
   } 
   log.Printf("Wrote %d bytes.\n", bytesWritten) 
} 

Quickly writing to a file

ioutil包有一个名为WriteFile()的有用函数,它将处理创建/打开、写入字节片和关闭。如果您只需要一种快速方式将字节片转储到文件中,则此功能非常有用:

package main 

import ( 
   "io/ioutil" 
   "log" 
) 

func main() { 
   err := ioutil.WriteFile("test.txt", []byte("Hi\n"), 0666) 
   if err != nil { 
      log.Fatal(err) 
   } 
} 

缓冲写入程序

bufio包允许您创建一个缓冲写入程序,以便在将缓冲写入磁盘之前使用内存中的缓冲区。如果您需要在将数据写入磁盘之前对数据进行大量操作,以节省磁盘 IO 的时间,则此功能非常有用。如果您一次只写入一个字节,并且希望在将其立即转储到文件之前将大量数据存储在内存缓冲区中,那么它也很有用,否则您将为每个字节执行磁盘 IO。这会对磁盘造成磨损,同时也会减慢进程。

可以检查缓冲写入程序,查看它当前存储了多少未缓冲的数据以及剩余的缓冲空间。缓冲区也可以重置以撤消自上次刷新以来的任何更改。缓冲区也可以调整大小。

下面的示例打开一个名为test.txt的文件,并创建一个打包文件对象的缓冲写入程序。将几个字节写入缓冲区,然后写入一个字符串。然后在将缓冲区的内容刷新到磁盘上的文件之前检查内存缓冲区。它还演示了如何重置缓冲区、撤消尚未刷新的任何更改,以及如何检查缓冲区中剩余的空间。最后,它演示了如何将缓冲区调整为特定大小:

package main 

import ( 
   "bufio" 
   "log" 
   "os" 
) 

func main() { 
   // Open file for writing 
   file, err := os.OpenFile("test.txt", os.O_WRONLY, 0666) 
   if err != nil { 
      log.Fatal(err) 
   } 
   defer file.Close() 

   // Create a buffered writer from the file 
   bufferedWriter := bufio.NewWriter(file) 

   // Write bytes to buffer 
   bytesWritten, err := bufferedWriter.Write( 
      []byte{65, 66, 67}, 
   ) 
   if err != nil { 
      log.Fatal(err) 
   } 
   log.Printf("Bytes written: %d\n", bytesWritten) 

   // Write string to buffer 
   // Also available are WriteRune() and WriteByte() 
   bytesWritten, err = bufferedWriter.WriteString( 
      "Buffered string\n", 
   ) 
   if err != nil { 
      log.Fatal(err) 
   } 
   log.Printf("Bytes written: %d\n", bytesWritten) 

   // Check how much is stored in buffer waiting 
   unflushedBufferSize := bufferedWriter.Buffered() 
   log.Printf("Bytes buffered: %d\n", unflushedBufferSize) 

   // See how much buffer is available 
   bytesAvailable := bufferedWriter.Available() 
   if err != nil { 
      log.Fatal(err) 
   } 
   log.Printf("Available buffer: %d\n", bytesAvailable) 

   // Write memory buffer to disk 
   bufferedWriter.Flush() 

   // Revert any changes done to buffer that have 
   // not yet been written to file with Flush() 
   // We just flushed, so there are no changes to revert 
   // The writer that you pass as an argument 
   // is where the buffer will output to, if you want 
   // to change to a new writer 
   bufferedWriter.Reset(bufferedWriter) 

   // See how much buffer is available 
   bytesAvailable = bufferedWriter.Available() 
   if err != nil { 
      log.Fatal(err) 
   } 
   log.Printf("Available buffer: %d\n", bytesAvailable) 

   // Resize buffer. The first argument is a writer 
   // where the buffer should output to. In this case 
   // we are using the same buffer. If we chose a number 
   // that was smaller than the existing buffer, like 10 
   // we would not get back a buffer of size 10, we will 
   // get back a buffer the size of the original since 
   // it was already large enough (default 4096) 
   bufferedWriter = bufio.NewWriterSize( 
      bufferedWriter, 
      8000, 
   ) 

   // Check available buffer size after resizing 
   bytesAvailable = bufferedWriter.Available() 
   if err != nil { 
      log.Fatal(err) 
   } 
   log.Printf("Available buffer: %d\n", bytesAvailable) 
} 

从文件中读取最多 n 个字节

os.File类型具有两个基本功能。其中一个是File.Read()Read(),它期望一个字节片作为参数传递。字节从文件中读取并放入字节片中。Read()将读取尽可能多的字节,或者直到缓冲区填满,然后停止读取。

在到达文件末尾之前,可能需要多次调用Read(),具体取决于提供的缓冲区大小和文件大小。如果在调用Read()期间到达文件末尾,则返回io.EOF错误:

package main 

import ( 
   "log" 
   "os" 
) 

func main() { 
   // Open file for reading 
   file, err := os.Open("test.txt") 
   if err != nil { 
      log.Fatal(err) 
   } 
   defer file.Close() 

   // Read up to len(b) bytes from the File 
   // Zero bytes written means end of file 
   // End of file returns error type io.EOF 
   byteSlice := make([]byte, 16) 
   bytesRead, err := file.Read(byteSlice) 
   if err != nil { 
      log.Fatal(err) 
   } 
   log.Printf("Number of bytes read: %d\n", bytesRead) 
   log.Printf("Data read: %s\n", byteSlice) 
} 

正读取 n 个字节

在上一个示例中,如果文件仅包含 10 个字节,File.Read()不会返回错误,但您提供了一个包含 500 个字节的字节片缓冲区。在某些情况下,您需要确保整个缓冲区都已填满。如果整个缓冲区未填满,io.ReadFull()函数将返回一个错误。如果io.ReadFull()没有任何数据可读取,则返回 EOF 错误。如果它读取了一些数据,但随后遇到 EOF,它将返回一个ErrUnexpectedEOF错误:

package main 

import ( 
   "io" 
   "log" 
   "os" 
) 

func main() { 
   // Open file for reading 
   file, err := os.Open("test.txt") 
   if err != nil { 
      log.Fatal(err) 
   } 

   // The file.Read() function will happily read a tiny file in to a    
   // large byte slice, but io.ReadFull() will return an 
   // error if the file is smaller than the byte slice. 
   byteSlice := make([]byte, 2) 
   numBytesRead, err := io.ReadFull(file, byteSlice) 
   if err != nil { 
      log.Fatal(err) 
   } 
   log.Printf("Number of bytes read: %d\n", numBytesRead) 
   log.Printf("Data read: %s\n", byteSlice) 
} 

读取至少 n 个字节

io包提供的另一个有用功能是io.ReadAtLeast()。如果至少没有指定字节数,这将返回一个错误。与io.ReadFull()类似,如果没有找到数据,则返回EOF错误;如果在遇到文件结尾之前读取了一些数据,则返回ErrUnexpectedEOF错误:

package main 

import ( 
   "io" 
   "log" 
   "os" 
) 

func main() { 
   // Open file for reading 
   file, err := os.Open("test.txt") 
   if err != nil { 
      log.Fatal(err) 
   } 

   byteSlice := make([]byte, 512) 
   minBytes := 8 
   // io.ReadAtLeast() will return an error if it cannot 
   // find at least minBytes to read. It will read as 
   // many bytes as byteSlice can hold. 
   numBytesRead, err := io.ReadAtLeast(file, byteSlice, minBytes) 
   if err != nil { 
      log.Fatal(err) 
   } 
   log.Printf("Number of bytes read: %d\n", numBytesRead) 
   log.Printf("Data read: %s\n", byteSlice) 
} 

Reading all bytes of a file

ioutil包提供了读取文件中每个字节并将其作为字节片返回的功能。此函数很方便,因为在执行读取之前不必定义字节片。缺点是,一个真正大的文件将返回一个可能比预期大的大切片。

io.ReadAll()函数需要一个已经用os.Open()Create()打开的文件:

package main 

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

func main() { 
   // Open file for reading 
   file, err := os.Open("test.txt") 
   if err != nil { 
      log.Fatal(err) 
   } 

   // os.File.Read(), io.ReadFull(), and 
   // io.ReadAtLeast() all work with a fixed 
   // byte slice that you make before you read 

   // ioutil.ReadAll() will read every byte 
   // from the reader (in this case a file), 
   // and return a slice of unknown slice 
   data, err := ioutil.ReadAll(file) 
   if err != nil { 
      log.Fatal(err) 
   } 

   fmt.Printf("Data as hex: %x\n", data) 
   fmt.Printf("Data as string: %s\n", data) 
   fmt.Println("Number of bytes read:", len(data)) 
} 

快速将整个文件读入内存

与上例中的io.ReadAll()函数类似,io.ReadFile()将读取文件中的所有字节并返回一个字节片。两者之间的主要区别在于io.ReadFile()需要的是文件路径,而不是已经打开的文件对象。io.ReadFile()功能将负责打开、读取和关闭文件。您只需提供一个文件名,它就提供了字节。这通常是加载文件数据的最快、最简单的方法。

虽然这种方法非常方便,但也有局限性;因为它将整个文件直接读取到内存中,所以非常大的文件可能会耗尽系统的内存限制:

package main 

import ( 
   "io/ioutil" 
   "log" 
) 

func main() { 
   // Read file to byte slice 
   data, err := ioutil.ReadFile("test.txt") 
   if err != nil { 
      log.Fatal(err) 
   } 

   log.Printf("Data read: %s\n", data) 
} 

缓冲读取器

创建一个缓冲读取器将存储一个包含一些内容的内存缓冲区。缓冲读取器还提供了一些在os.Fileio.Reader类型上不可用的更多功能。默认缓冲区大小为 4096,最小大小为 16。缓冲读取器提供了一组有用的函数。一些可用功能包括但不限于以下:

  • Read():将数据读入字节片
  • Peek():在不移动文件光标的情况下检查下一个字节
  • ReadByte(): This is to read a single byte
  • UnreadByte():这未读取读取的最后一个字节
  • ReadBytes():读取字节,直到达到指定的分隔符
  • ReadString():读取字符串,直到达到指定的分隔符

下面的示例演示如何使用缓冲读取器从文件中获取数据。首先,它打开一个文件,然后创建一个打包文件对象的缓冲读取器。缓冲读取器准备就绪后,将显示如何使用上述功能:

package main 

import ( 
   "bufio" 
   "fmt" 
   "log" 
   "os" 
) 

func main() { 
   // Open file and create a buffered reader on top 
   file, err := os.Open("test.txt") 
   if err != nil { 
      log.Fatal(err) 
   } 
   bufferedReader := bufio.NewReader(file) 

   // Get bytes without advancing pointer 
   byteSlice := make([]byte, 5) 
   byteSlice, err = bufferedReader.Peek(5) 
   if err != nil { 
      log.Fatal(err) 
   } 
   fmt.Printf("Peeked at 5 bytes: %s\n", byteSlice) 

   // Read and advance pointer 
   numBytesRead, err := bufferedReader.Read(byteSlice) 
   if err != nil { 
      log.Fatal(err) 
   } 
   fmt.Printf("Read %d bytes: %s\n", numBytesRead, byteSlice) 

   // Ready 1 byte. Error if no byte to read 
   myByte, err := bufferedReader.ReadByte() 
   if err != nil { 
      log.Fatal(err) 
   }  
   fmt.Printf("Read 1 byte: %c\n", myByte) 

   // Read up to and including delimiter 
   // Returns byte slice 
   dataBytes, err := bufferedReader.ReadBytes('\n') 
   if err != nil { 
      log.Fatal(err) 
   } 
   fmt.Printf("Read bytes: %s\n", dataBytes) 

   // Read up to and including delimiter 
   // Returns string 
   dataString, err := bufferedReader.ReadString('\n') 
   if err != nil { 
      log.Fatal(err) 
   } 
   fmt.Printf("Read string: %s\n", dataString) 

   // This example reads a few lines so test.txt 
   // should have a few lines of text to work correct 
} 

用扫描仪阅读

扫描仪是bufio包的一部分。它对于以特定分隔符单步遍历文件很有用。通常,换行符用作分隔符,用于按行分隔文件。在 CSV 文件中,逗号将是分隔符。os.File对象可以像缓冲读取器一样包装在bufio.Scanner对象中。我们将调用Scan()读取下一个分隔符,然后使用Text()Bytes()获取读取的数据。

The delimiter is not just a simple byte or character. There is actually a special function, which you have to implement, that will determine where the next delimiter is, how far forward to advance the pointer, and what data to return. If no custom SplitFunc type is provided, it defaults to ScanLines, which will split at every newline character. Other split functions included in bufio are ScanRunes and ScanWords.

要定义自己的分割函数,请定义与此指纹匹配的函数:

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

返回(0nilnil)将通知扫描仪再次扫描,但缓冲区较大,因为没有足够的数据到达分隔符。

In the following example, bufio.Scanner is created from the file, and then the file is scanned word by word:

package main 

import ( 
   "bufio" 
   "fmt" 
   "log" 
   "os" 
) 

func main() { 
   // Open file and create scanner on top of it 
   file, err := os.Open("test.txt") 
   if err != nil { 
      log.Fatal(err) 
   } 
   scanner := bufio.NewScanner(file) 

   // Default scanner is bufio.ScanLines. Lets use ScanWords. 
   // Could also use a custom function of SplitFunc type 
   scanner.Split(bufio.ScanWords) 

   // Scan for next token. 
   success := scanner.Scan() 
   if success == false { 
      // False on error or EOF. Check error 
      err = scanner.Err() 
      if err == nil { 
         log.Println("Scan completed and reached EOF") 
      } else { 
         log.Fatal(err) 
      } 
   } 

   // Get data from scan with Bytes() or Text() 
   fmt.Println("First word found:", scanner.Text()) 

   // Call scanner.Scan() manually, or loop with for 
   for scanner.Scan() { 
      fmt.Println(scanner.Text()) 
   } 
} 

档案

归档文件是一种存储多个文件的文件格式。两种最常见的归档格式是 tar-ZIP 归档。Go 标准库既有tar包,也有zip包。这些示例使用 ZIP 格式,但 tar 格式很容易互换。

存档(ZIP)文件

下面的示例演示如何创建包含多个文件的存档。示例中的文件仅用几个字节硬编码,但应易于调整以满足其他需要:

// This example uses zip but standard library 
// also supports tar archives 
package main 

import ( 
   "archive/zip" 
   "log" 
   "os" 
) 

func main() { 
   // Create a file to write the archive buffer to 
   // Could also use an in memory buffer. 
   outFile, err := os.Create("test.zip") 
   if err != nil { 
      log.Fatal(err) 
   } 
   defer outFile.Close() 

   // Create a zip writer on top of the file writer 
   zipWriter := zip.NewWriter(outFile) 

   // Add files to archive 
   // We use some hard coded data to demonstrate, 
   // but you could iterate through all the files 
   // in a directory and pass the name and contents 
   // of each file, or you can take data from your 
   // program and write it write in to the archive without 
   var filesToArchive = []struct { 
      Name, Body string 
   }{ 
      {"test.txt", "String contents of file"}, 
      {"test2.txt", "\x61\x62\x63\n"}, 
   } 

   // Create and write files to the archive, which in turn 
   // are getting written to the underlying writer to the 
   // .zip file we created at the beginning 
   for _, file := range filesToArchive { 
      fileWriter, err := zipWriter.Create(file.Name) 
      if err != nil { 
         log.Fatal(err) 
      } 
      _, err = fileWriter.Write([]byte(file.Body)) 
      if err != nil { 
         log.Fatal(err) 
      } 
   } 

   // Clean up 
   err = zipWriter.Close() 
   if err != nil { 
      log.Fatal(err) 
   } 
} 

提取(解压缩)存档文件

下面的示例演示如何取消归档 ZIP 格式的文件。如有必要,它将通过创建目录复制在存档中找到的目录结构:

// This example uses zip but standard library 
// also supports tar archives 
package main 

import ( 
   "archive/zip" 
   "io" 
   "log" 
   "os" 
   "path/filepath" 
) 

func main() { 
   // Create a reader out of the zip archive 
   zipReader, err := zip.OpenReader("test.zip") 
   if err != nil { 
      log.Fatal(err) 
   } 
   defer zipReader.Close() 

   // Iterate through each file/dir found in 
   for _, file := range zipReader.Reader.File { 
      // Open the file inside the zip archive 
      // like a normal file 
      zippedFile, err := file.Open() 
      if err != nil { 
         log.Fatal(err) 
      } 
      defer zippedFile.Close() 

      // Specify what the extracted file name should be. 
      // You can specify a full path or a prefix 
      // to move it to a different directory. 
      // In this case, we will extract the file from 
      // the zip to a file of the same name. 
      targetDir := "./" 
      extractedFilePath := filepath.Join( 
         targetDir, 
         file.Name, 
      ) 

      // Extract the item (or create directory) 
      if file.FileInfo().IsDir() { 
         // Create directories to recreate directory 
         // structure inside the zip archive. Also 
         // preserves permissions 
         log.Println("Creating directory:", extractedFilePath) 
         os.MkdirAll(extractedFilePath, file.Mode()) 
      } else { 
         // Extract regular file since not a directory 
         log.Println("Extracting file:", file.Name) 

         // Open an output file for writing 
         outputFile, err := os.OpenFile( 
            extractedFilePath, 
            os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 
            file.Mode(), 
         ) 
         if err != nil { 
            log.Fatal(err) 
         } 
         defer outputFile.Close() 

         // "Extract" the file by copying zipped file 
         // contents to the output file 
         _, err = io.Copy(outputFile, zippedFile) 
         if err != nil { 
            log.Fatal(err) 
         } 
      }  
   } 
} 

压缩

Go 标准库还支持压缩,这与归档不同。通常,归档和压缩相结合,将大量文件打包到单个压缩文件中。最常见的格式可能是.tar.gz文件,它是一个 gzip tar ball。不要混淆 zip 和 gzip,因为它们是两个不同的东西。

Go 标准库支持多种压缩算法:

  • bzip2: bzip2 format
  • 公寓:放气(RFC 1951)
  • gzip:gzip 格式(RFC1952)
  • lzw:来自高性能数据压缩技术的 Lempel-Ziv-Welch 格式,计算机,17(6)(1984 年 6 月),第 8-19 页
  • zlib: zlib format (RFC 1950)

上阅读更多关于每个包装的信息 https://golang.org/pkg/compress/ 。这些示例使用 gzip 压缩,但交换上述任何包都应该很容易。

压缩文件

以下示例演示如何使用gzip包压缩文件:

// This example uses gzip but standard library also 
// supports zlib, bz2, flate, and lzw 
package main 

import ( 
   "compress/gzip" 
   "log" 
   "os" 
) 

func main() { 
   // Create .gz file to write to 
   outputFile, err := os.Create("test.txt.gz") 
   if err != nil { 
      log.Fatal(err) 
   } 

   // Create a gzip writer on top of file writer 
   gzipWriter := gzip.NewWriter(outputFile) 
   defer gzipWriter.Close() 

   // When we write to the gzip writer 
   // it will in turn compress the contents 
   // and then write it to the underlying 
   // file writer as well 
   // We don't have to worry about how all 
   // the compression works since we just 
   // use it as a simple writer interface 
   // that we send bytes to 
   _, err = gzipWriter.Write([]byte("Gophers rule!\n")) 
   if err != nil { 
      log.Fatal(err) 
   } 

   log.Println("Compressed data written to file.") 
} 

解压缩文件

The following example demonstrates how to uncompress a file using the gzip algorithm:

// This example uses gzip but standard library also 
// supports zlib, bz2, flate, and lzw 
package main 

import ( 
   "compress/gzip" 
   "io" 
   "log" 
   "os" 
) 

func main() { 
   // Open gzip file that we want to uncompress 
   // The file is a reader, but we could use any 
   // data source. It is common for web servers 
   // to return gzipped contents to save bandwidth 
   // and in that case the data is not in a file 
   // on the file system but is in a memory buffer 
   gzipFile, err := os.Open("test.txt.gz") 
   if err != nil { 
      log.Fatal(err) 
   } 

   // Create a gzip reader on top of the file reader 
   // Again, it could be any type reader though 
   gzipReader, err := gzip.NewReader(gzipFile) 
   if err != nil { 
      log.Fatal(err) 
   } 
   defer gzipReader.Close() 

   // Uncompress to a writer. We'll use a file writer 
   outfileWriter, err := os.Create("unzipped.txt") 
   if err != nil { 
      log.Fatal(err) 
   } 
   defer outfileWriter.Close() 

   // Copy contents of gzipped file to output file 
   _, err = io.Copy(outfileWriter, gzipReader) 
   if err != nil { 
      log.Fatal(err) 
   } 
} 

在我们结束本章有关使用文件的内容之前,让我们看两个可能有用的更实际的例子。当您不想创建永久文件,但需要使用文件时,临时文件和目录非常有用。此外,获取文件的常见方法是通过互联网下载。下面的示例演示这些操作。

创建临时文件和目录

ioutil包提供两个功能:TempDir()TempFile()。完成后,来电者有责任删除临时项目。这些函数提供的唯一好处是,您可以为目录传递一个空字符串,它将自动在系统的默认临时文件夹(/tmp在 Linux 上)中创建该项,因为os.TempDir()函数将返回默认的系统临时目录:

package main 

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

func main() { 
   // Create a temp dir in the system default temp folder 
   tempDirPath, err := ioutil.TempDir("", "myTempDir") 
   if err != nil { 
      log.Fatal(err) 
   } 
   fmt.Println("Temp dir created:", tempDirPath) 

   // Create a file in new temp directory 
   tempFile, err := ioutil.TempFile(tempDirPath, "myTempFile.txt") 
   if err != nil { 
      log.Fatal(err) 
   } 
   fmt.Println("Temp file created:", tempFile.Name()) 

   // ... do something with temp file/dir ... 

   // Close file 
   err = tempFile.Close() 
   if err != nil { 
      log.Fatal(err) 
   } 

   // Delete the resources we created 
   err = os.Remove(tempFile.Name()) 
   if err != nil { 
      log.Fatal(err) 
   } 
   err = os.Remove(tempDirPath) 
   if err != nil { 
      log.Fatal(err) 
   } 
}

通过 HTTP 下载文件

现代计算中的一项常见任务是通过 HTTP 协议下载文件。下面的示例演示如何快速将特定 URL 下载到文件。

完成此任务的其他常用工具有curlwget

package main 

import ( 
   "io" 
   "log" 
   "net/http" 
   "os" 
) 

func main() { 
   // Create output file 
   newFile, err := os.Create("devdungeon.html") 
   if err != nil { 
      log.Fatal(err) 
   } 
   defer newFile.Close() 

   // HTTP GET request devdungeon.com 
   url := "http://www.devdungeon.com/archive" 
   response, err := http.Get(url) 
   defer response.Body.Close() 

   // Write bytes from HTTP response to file. 
   // response.Body satisfies the reader interface. 
   // newFile satisfies the writer interface. 
   // That allows us to use io.Copy which accepts 
   // any type that implements reader and writer interface 
   numBytesWritten, err := io.Copy(newFile, response.Body) 
   if err != nil { 
      log.Fatal(err) 
   } 
   log.Printf("Downloaded %d byte file.\n", numBytesWritten) 
} 

总结

阅读本章后,您现在应该熟悉了与文件交互的一些不同方式,并对执行基本操作感到满意。我们的目标不是记住所有这些函数名,而是了解可用的工具。如果您需要示例代码,本章可以作为参考,但我鼓励您使用以下代码片段创建一个食谱存储库。

有用的文件函数分布在多个包中。os包仅包含用于处理文件的基本操作,如打开、关闭和简单读取。io包提供的功能可用于比os包更高级别的读写器接口。ioutil软件包为处理文件提供了更高级的便利功能。

在下一章中,我们将讨论取证的主题。它将包括查找异常文件等内容,这些文件非常大或最近修改过。除了文件取证,我们还将介绍一些网络取证调查主题,即查找主机的主机名、IP 和 MX 记录。取证一章还介绍了隐写术的基本示例,展示了如何在图像中隐藏数据以及如何在图像中找到隐藏数据。