Skip to content

Files

Latest commit

4475ace · Oct 25, 2021

History

History
1986 lines (1515 loc) · 59 KB

File metadata and controls

1986 lines (1515 loc) · 59 KB

六、文件输入和输出

在上一章中,我们讨论了将文件和目录作为实体进行操作,而不查看其内容。然而,在这一章中,我们将采取不同的方法,并研究文件的内容:您可能会认为本章是本书中最重要的章节之一,因为损坏文件的输入是:任何一个操作系统的主要任务。

本章的主要目的是教授 Go 标准库如何允许我们打开文件、读取文件内容、处理文件(如果需要)、创建新文件以及将所需数据放入文件中。有两种主要的文件读写方式:使用io包和使用bufio包的功能。然而,这两个软件包都是以比较的方式工作的。

本章将向您介绍以下内容:

  • 打开文件进行写入和读取
  • 使用io包进行文件输入和输出
  • 使用io.Writerio.Reader接口
  • 使用bufio包进行缓冲输入和输出
  • 在 Go 中复制文件
  • 在 Go 中实现一个版本的wc(1)实用程序
  • 在 Go 中开发一个版本的dd(1)命令
  • 创建稀疏文件
  • 字节片在文件输入和输出中的重要性:第 2 章在 Go中编写程序时首先提到了字节片
  • 将结构化数据存储在文件中,然后读取
  • 将制表符转换为空格字符,反之亦然

本章不会讨论将数据附加到现有文件中:您必须等到第 7 章处理系统文件,才能了解更多关于在不破坏现有数据的情况下将数据放在文件末尾的信息。

关于文件输入和输出

文件输入和输出包括与读取文件数据和将所需数据写入文件有关的所有内容。没有一个操作系统不支持文件,因此也不支持文件输入和输出。

由于这一章相当大,我将停止谈论,并开始向您展示实用的 Go 代码,这将使事情变得更清楚。因此,在本章中,您将学习的第一件事是字节片,这在涉及文件输入和输出的应用中非常重要。

字节片

字节片是一种用于文件读写的片。简单地说,它们是在文件读写操作期间用作缓冲区的字节片。本节将介绍一个小型 Go 示例,其中字节片用于写入文件和读取文件。正如您将在本章中看到的字节片一样,请确保您理解所给出的示例。相关 Go 代码另存为byteSlice.go,将分三部分呈现。

第一部分内容如下:

package main 

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

byteSlice.go的第二部分如下:

func main() { 
   if len(os.Args) != 2 { 
         fmt.Println("Please provide a filename") 
         os.Exit(1) 
   } 
   filename := os.Args[1] 

   aByteSlice := []byte("Mihalis Tsoukalos!\n") 
   ioutil.WriteFile(filename, aByteSlice, 0644) 

在这里,您使用aByteSlice字节片将一些文本保存到由filename变量标识的文件中。byteSlice.go的最后一部分是以下 Go 代码:

   f, err := os.Open(filename) 
   if err != nil { 
         fmt.Println(err) 
         os.Exit(1) 
   } 
   defer f.Close() 

   anotherByteSlice := make([]byte, 100) 
   n, err := f.Read(anotherByteSlice) 
   fmt.Printf("Read %d bytes: %s", n, anotherByteSlice)

} 

在这里,您定义了另一个名为anotherByteSlice的字节片,其中有100个位置,将用于从先前创建的文件中读取。请注意,fmt.Printf()中使用的%s强制anotherByteSlice作为字符串打印:使用Println()将产生完全不同的输出。

请注意,由于文件较小,f.Read()调用会将较少的数据放入anotherByteSlice

anotherByteSlice的大小表示在对Read()进行一次调用或从文件中读取数据的任何其他类似操作后,可以存储到其中的最大数据量。

执行byteSlice.go将生成以下输出:

$ go run byteSlice.go usingByteSlices
Read 19 bytes: Mihalis Tsoukalos!

检查usingByteSlices文件的大小将验证写入该文件的数据量是否正确:

$ wc usingByteSlices
   1   2  19 usingByteSlices

关于二进制文件

在 Go 中读取和写入二进制和纯文本文件没有区别。因此,在处理文件时,Go 对其格式不作任何假设。然而,Go 提供了一个名为 binary 的软件包,允许您在不同的编码之间进行翻译,例如小端码大端码

readBinary.go文件简要说明了如何将整数转换为小端数和大端数,当您要处理的文件包含某些类型的数据时,这可能很有用;这主要发生在我们处理原始设备和原始数据包操作时:记住,一切都是一个文件!readBinary.go的源代码将分两部分介绍。

第一部分内容如下:

package main 

import ( 
   "bytes" 
   "encoding/binary" 
   "fmt" 
   "os" 
   "strconv" 
) 

func main() { 
   if len(os.Args) != 2 { 
         fmt.Println("Please provide an integer") 
         os.Exit(1) 
   } 
   aNumber, _ := strconv.ParseInt(os.Args[1], 10, 64) 

这部分节目没有什么特别之处。第二部分如下:

   buf := new(bytes.Buffer) 
   err := binary.Write(buf, binary.LittleEndian, aNumber) 
   if err != nil { 
         fmt.Println("Little Endian:", err) 
   } 

   fmt.Printf("%d is %x in Little Endian\n", aNumber, buf) 
   buf.Reset() 
   err = binary.Write(buf, binary.BigEndian, aNumber)

   if err != nil { 
         fmt.Println("Big Endian:", err) 
   } 
   fmt.Printf("And %x in Big Endian\n", buf) 
} 

第二部分包含所有重要的 Go 代码:转换借助于binary.Write()方法和适当的写入参数(binary.LittleEndianbinary.BigEndian进行。bytes.Buffer变量用于程序的io.Readerio.Writer接口。最后,buf.Reset()语句重置缓冲区,以便以后用于存储 big-endian。

执行readBinary.go将生成以下输出:

$ go run readBinary.go 1
1 is 0100000000000000 in Little Endian
And 0000000000000001 in Big Endian

您可以通过访问其文档页面找到有关二进制软件包的更多信息 https://golang.org/pkg/encoding/binary/

Go中有用的 I/O 包

io包用于执行原始文件 I/O 操作,bufio包用于执行缓冲 I/O。

在缓冲 I/O 中,操作系统在文件读写操作期间使用中间缓冲区,以减少文件系统调用的数量。因此,缓冲输入和输出速度更快、效率更高。

此外,您还可以使用fmt包的一些函数将文本写入文件。请注意,flag包也将在本章中使用,以及在开发的实用程序需要支持命令行标志的所有后续应用中使用。

io 包

io包提供了允许您写入或读取文件的功能。其使用将在usingIO.go文件中进行说明,该文件将分为三个部分。程序所做的是从文件中读取8字节,并将其写入标准输出。

第一部分是 Go 计划的前言:

package main 

import ( 
   "fmt" 
   "io" 
   "os" 
) 

第二部分是以下 Go 代码:

func main() { 
   if len(os.Args) != 2 { 
         fmt.Println("Please provide a filename") 
         os.Exit(1) 
   } 

   filename := os.Args[1] 
   f, err := os.Open(filename) 
   if err != nil { 
         fmt.Printf("error opening %s: %s", filename, err) 
         os.Exit(1) 
   } 
   defer f.Close() 

该程序还使用方便的defer命令,将函数的执行推迟到周围函数返回。因此,defer在文件 I/O 操作中使用得非常频繁,因为它使您不必在处理完文件后,或在使用return语句或os.Exit()将函数保留在任意位置时记住执行Close()调用。

计划的最后一部分如下:

   buf := make([]byte, 8) 
   if _, err := io.ReadFull(f, buf); err != nil { 
         if err == io.EOF { 
               err = io.ErrUnexpectedEOF 
         } 
   } 
   io.WriteString(os.Stdout, string(buf)) 
   fmt.Println() 
} 

这里的io.ReadFull()函数从打开文件的读取器读取数据,并将数据放入一个有 8 个位置的字节片中。您还可以在这里看到使用io.WriteString()功能将数据打印到标准输出(os.Stdout也是一个文件)。然而,这不是一个非常常见的做法,因为您可以简单地使用fmt.Println()来代替。

执行usingIO.go生成以下输出:

$ go run usingIO.go usingByteSlices
Mihalis

布菲奥方案

bufio包的功能允许您执行缓冲文件操作,这意味着尽管其操作看起来与io中的操作相似,但它们的工作方式略有不同。

bufio实际做的是将io.Readerio.Writer对象包装成一个新值,实现所需的接口,同时为新值提供缓冲。bufio软件包的一个便利功能是,它允许您逐行、逐字、逐字地阅读文本文件,而无需花费太多精力。

再一次,一个例子将试图澄清一些事情:展示bufio用法的 Go 文件的名称是bufIO.go,将分四个部分介绍。

第一部分是预期的序言:

package main 

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

第二部分如下:

func main() { 
   if len(os.Args) != 2 { 
         fmt.Println("Please provide a filename") 
         os.Exit(1) 
   } 

   filename := os.Args[1] 

在这里,您只需尝试获取要使用的文件名。

bufIO.go的第三部分有以下 Go 代码:

   f, err := os.Open(filename) 
   if err != nil { 
         fmt.Printf("error opening %s: %s", filename, err) 
         os.Exit(1) 
   } 
   defer f.Close() 

   scanner := bufio.NewScanner(f) 

bufio.NewScanner的默认行为是逐行读取其输入,这意味着每次调用读取下一个令牌的Scan()方法时,都会返回一个新行。最后一部分是调用Scan()方法以读取文件的全部内容:

   for scanner.Scan() { 
         line := scanner.Text() 

         if scanner.Err() != nil { 
               fmt.Printf("error reading file %s", err) 
               os.Exit(1) 
         } 
         fmt.Println(line) 
   } 
}

Text()方法以字符串形式返回Scan()方法中的最新标记,在本例中,该字符串将是一行。但是,如果您在逐行读取文件时遇到奇怪的结果,则很可能是文件的行尾,这通常是来自 Windows 计算机的文本文件的情况。

执行bufIO.go并用其输出馈送wc(1)可以帮助您验证bufIO.go是否按预期工作:

$ go run bufIO.go inputFile | wc
      11      12      62
$ wc inputFile
      11      12      62 inputFile

文件 I/O 操作

现在,您已经了解了iobufio包的基本知识,现在是时候了解有关它们的用法以及它们如何帮助您处理文件的更详细信息了。但首先,我们将讨论fmt.Fprintf()函数。

使用 fmt.Fprintf()写入文件

使用fmt.Fprintf()函数可以将格式化文本以类似于fmt.Printf()函数的方式写入文件。请注意,fmt.Fprintf()可以写入任何io.Writer接口,我们的文件将满足io.Writer接口。

说明fmt.Fprintf()用法的 Go 代码可在fmtF.go中找到,该代码将分为三部分。第一部分是预期的序言:

package main 

import ( 
   "fmt" 
   "os" 
) 

第二部分具有以下 Go 代码:

func main() { 
   if len(os.Args) != 2 { 
         fmt.Println("Please provide a filename") 
         os.Exit(1) 
   } 

   filename := os.Args[1] 
   destination, err := os.Create(filename) 
   if err != nil { 
         fmt.Println("os.Create:", err) 
         os.Exit(1) 
   } 
   defer destination.Close() 

请注意,os.Create()函数将截断已存在的文件。

最后一部分是以下内容:

   fmt.Fprintf(destination, "[%s]: ", filename) 
   fmt.Fprintf(destination, "Using fmt.Fprintf in %s\n", filename) 
} 

在这里,您使用fmt.Fprintf()将所需的文本数据写入目标变量标识的文件,就像使用fmt.Printf()方法一样。

执行fmtF.go将生成以下输出:

$ go run fmtF.go test
$ cat test
[test]: Using fmt.Fprintf in test 

换句话说,您可以使用fmt.Fprintf()创建纯文本文件。

关于 io.Writer 和 io.Reader

io.Writerio.Reader都是分别嵌入io.Write()io.Read()方法的接口。io.Writerio.Reader的使用将在readerWriter.go中进行说明,将分四部分介绍。该程序计算其输入文件的字符,并将字符数写入另一个文件:如果处理的是 Unicode 字符,每个字符占用一个以上字节,则可能会认为程序正在读取字节。输出文件名为原始文件名加上.Count扩展名。

第一部分如下:

package main 

import ( 
   "fmt" 
   "io" 
   "os" 
) 

第二部分如下:

func countChars(r io.Reader) int { 
   buf := make([]byte, 16) 
   total := 0 
   for { 
         n, err := r.Read(buf) 
         if err != nil && err != io.EOF { 
               return 0 
         } 
         if err == io.EOF { 
               break 
         } 
         total = total + n 
   } 
   return total 
} 

同样,在读取期间使用字节片。break语句允许您退出for循环。第三部分是以下代码:

func writeNumberOfChars(w io.Writer, x int) { 
   fmt.Fprintf(w, "%d\n", x) 
} 

在这里,您可以看到如何使用fmt.Fprintf()将数字写入文件:我没有使用字节片实现同样的操作!另外,请注意,所提供的代码使用一个io.Writer变量(w)将文本写入文件。

readerWriter.go的最后一部分有以下 Go 代码:

func main() { 
   if len(os.Args) != 2 { 
         fmt.Println("Please provide a filename") 
         os.Exit(1) 
   } 

   filename := os.Args[1] 
   _, err := os.Stat(filename)

   if err != nil { 
         fmt.Printf("Error on file %s: %s\n", filename, err) 
         os.Exit(1) 
   } 

   f, err := os.Open(filename) 
   if err != nil { 
         fmt.Println("Cannot open file:", err) 
         os.Exit(-1) 
   } 
   defer f.Close() 

   chars := countChars(f) 
   filename = filename + ".Count" 
   f, err = os.Create(filename) 
   if err != nil { 
         fmt.Println("os.Create:", err) 
         os.Exit(1) 
   } 
   defer f.Close() 
   writeNumberOfChars(f, chars) 
} 

readerWriter.go的执行不产生输出;因此,由您检查其正确性,在本例中,这是在wc(1)的帮助下发生的:

$ go run readerWriter.go /tmp/swtag.log
$ wc /tmp/swtag.log
     119     635    7780 /tmp/swtag.log
$ cat /tmp/swtag.log.Count
7780

找出一行的第三列

既然您知道如何读取文件,现在是时候展示您在第三章高级Go功能中看到的readColumn.go程序的修改版本了。新版本也被命名为readColumn.go,但有两个主要改进。第一个是您可以提供所需的列作为命令行参数,第二个是如果它获得多个命令行参数,它可以读取多个文件。

readColumn.go文件将分为三个部分。readColumn.go的第一部分如下:

package main 

import ( 
   "bufio" 
   "flag" 
   "fmt" 
   "io" 
   "os" 
   "strings" 
) 

readColumn.go的下一部分包含以下 Go 代码:

func main() { 
   minusCOL := flag.Int("COL", 1, "Column") 
   flag.Parse() 
   flags := flag.Args() 

   if len(flags) == 0 { 
         fmt.Printf("usage: readColumn <file1> [<file2> [... <fileN]]\n") 
         os.Exit(1) 
   } 

   column := *minusCOL 

   if column < 0 { 
         fmt.Println("Invalid Column number!") 
         os.Exit(1) 
   } 

minusCOL变量的定义可以理解,如果用户不使用此标志,程序将打印其读取的每个文件的第一列内容。

readColumn.go的最后一部分如下:

   for _, filename := range flags { 
         fmt.Println("\t\t", filename) 
         f, err := os.Open(filename) 
         if err != nil { 
               fmt.Printf("error opening file %s", err) 
               continue 
         } 
         defer f.Close() 

         r := bufio.NewReader(f)

         for { 
               line, err := r.ReadString('\n') 

               if err == io.EOF { 
                     break 
               } else if err != nil { 
                     fmt.Printf("error reading file %s", err) 
               } 

               data := strings.Fields(line) 
               if len(data) >= column { 
                     fmt.Println((data[column-1])) 
               } 
         } 
   } 
} 

前面的代码不会执行您以前未见过的任何操作。for循环用于处理所有命令行参数。但是,如果某个文件由于某种原因无法打开,程序将不会停止其执行,但它将继续处理其余文件(如果存在)。然而,程序期望其输入文件以换行结束,如果输入文件以不同的方式结束,您可能会看到奇怪的结果。

执行readColumn.go生成以下输出,为了节省一些书籍空间,将其缩写为:

$ go run readColumn.go -COL=3 pF.data isThereAFile up.data
            pF.data
            isThereAFile
error opening file open isThereAFile: no such file or directory
            up.data
0.05
0.05
0.05
0.05
0.05
0.05

在本例中,没有名为isThereAFile的文件,pF.data文件没有第三列。然而,该计划尽了最大努力,尽其所能打印出来!

在 Go 中复制文件

每个操作系统都允许您复制文件,因为这是一个非常重要和必要的操作。本节将向您展示如何在 Go 中复制文件,因为您已经知道如何读取文件!

复制文件的方法不止一种!

大多数编程语言提供了不止一种创建文件副本的方法,Go 也不例外。由开发人员决定实现哪种方法。

这里的 t不止一种方法规则适用于本书中实现的几乎所有内容,但文件复制是该规则最典型的例子,因为您可以通过逐行、逐字节或同时读取文件来复制文件!但是,此规则不适用于 Go 喜欢格式化其代码的方式!

复制文本文件

除非您希望检查或修改文本文件的内容,否则以特殊方式处理文本文件的复制是没有意义的。因此,这里介绍的三种技术不会区分纯文本和二进制文件复制。

第 7 章处理系统文件时,将讨论文件权限,因为有时您希望使用您选择的文件权限创建新文件。

使用 io.Copy

本小节将介绍一种使用io.Copy()功能复制文件的技术。io.Copy()函数的特殊之处在于,在这个过程中,is 并没有给您任何灵活性。节目名称为notGoodCP.go,分三部分介绍。请注意,notGoodCP.go更合适的文件名应该是copyEntireFileAtOnce.gocopyByReadingInputFileAllAtOnce.go

notGoodCP.go的 Go 代码的第一部分如下:

package main 

import ( 
   "fmt" 
   "io" 
   "os" 
) 

第二部分内容如下:

func Copy(src, dst string) (int64, error) { 
   sourceFileStat, err := os.Stat(src) 
   if err != nil { 
         return 0, err 
   } 

   if !sourceFileStat.Mode().IsRegular() { 
         return 0, fmt.Errorf("%s is not a regular file", src) 
   } 

   source, err := os.Open(src) 
   if err != nil { 
         return 0, err 
   } 
   defer source.Close() 

   destination, err := os.Create(dst) 
   if err != nil { 
         return 0, err 
   } 
   defer destination.Close() 
   nBytes, err := io.Copy(destination, source) 
   return nBytes, err 

}

这里我们定义了我们自己的函数,它使用io.Copy()制作文件的副本。Copy()函数在尝试复制源文件之前检查源文件是否为常规文件,这非常有意义。

最后一部分是main()功能的实现:

func main() { 
   if len(os.Args) != 3 { 
         fmt.Println("Please provide two command line arguments!") 
         os.Exit(1) 
   } 

   sourceFile := os.Args[1] 
   destinationFile := os.Args[2] 
   nBytes, err := Copy(sourceFile, destinationFile) 

   if err != nil { 
         fmt.Printf("The copy operation failed %q\n", err) 
   } else { 
         fmt.Printf("Copied %d bytes!\n", nBytes) 
   } 
} 

测试一个文件是否是另一个文件的精确副本的最佳工具是diff(1)实用程序,它也适用于二进制文件。您可以通过阅读diff(1)的主页了解更多信息。

执行notGoodCP.go将产生以下结果:

$ go run notGoodCP.go testFile aCopy
Copied 871 bytes!
$ diff aCopy testFile
$ wc testFile aCopy
      51     127     871 testFile
      51     127     871 aCopy
     102     254    1742 total

一次读取一个文件!

本节中的技术将使用ioutil.WriteFile()ioutil.ReadFile()功能。注意,ioutil.ReadFile()没有实现io.Reader接口,因此有点限制。

本节的 Go 代码名为readAll.go,将分三部分介绍。

第一部分具有以下 Go 代码:

package main 

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

第二部分如下:

func main() { 
   if len(os.Args) != 3 { 
         fmt.Println("Please provide two command line arguments!") 
         os.Exit(1) 
   } 

   sourceFile := os.Args[1] 
   destinationFile := os.Args[2] 

最后一部分内容如下:

   input, err := ioutil.ReadFile(sourceFile) 
   if err != nil { 
         fmt.Println(err) 
         os.Exit(1) 
   } 

   err = ioutil.WriteFile(destinationFile, input, 0644) 
   if err != nil { 
         fmt.Println("Error creating the new file", destinationFile) 
         fmt.Println(err) 
         os.Exit(1) 
   } 
} 

请注意,ioutil.ReadFile()函数读取整个文件,当您想要复制大型文件时,这可能不会有效。类似地,ioutil.WriteFile()函数将所有给定数据写入由其第一个参数标识的文件。

执行readAll.go产生以下输出:

$ go run readAll.go testFile aCopy
$ diff aCopy testFile
$ ls -l testFile aCopy
-rw-r--r--  1 mtsouk  staff  871 May  3 21:07 aCopy
-rw-r--r--@ 1 mtsouk  staff  871 May  3 21:04 testFile
$ go run readAll.go doesNotExist aCopy
open doesNotExist: no such file or directory
exit status 1

更好的文件复制程序

本节将介绍一个使用更传统方法的程序,其中缓冲区用于读取和复制新文件。

尽管传统的 Unix 命令行实用程序在没有错误时是无声的,但在您自己的工具中打印某种信息(如读取的字节数)也不错。但是,正确的做法是遵循 Unix 方式。

有两个主要原因使得cp.go优于notGoodCP.go。首先,开发人员对流程有更多的控制权,以换取编写更多的 Go 代码;其次,cp.go允许您定义缓冲区的大小,这是复制操作中最重要的参数。

cp.go的代码将分五部分介绍。第一部分是预期的前导码以及保存读取缓冲区大小的全局变量:

package main 

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

var BUFFERSIZE int64 

第二部分如下:

func Copy(src, dst string, BUFFERSIZE int64) error { 
   sourceFileStat, err := os.Stat(src) 
   if err != nil { 
         return err 
   } 

   if !sourceFileStat.Mode().IsRegular() { 
         return fmt.Errorf("%s is not a regular file.", src) 
   } 

   source, err := os.Open(src) 
   if err != nil { 
         return err 
   } 
   defer source.Close() 

正如您在这里看到的,缓冲区的大小作为一个参数提供给Copy()函数。另外两个命令行参数是输入文件名和输出文件名。

第三部分有Copy()功能的剩余 Go 代码:

   _, err = os.Stat(dst) 
   if err == nil { 
         return fmt.Errorf("File %s already exists.", dst) 
   } 

   destination, err := os.Create(dst) 
   if err != nil { 
         return err 
   } 
   defer destination.Close() 

   if err != nil { 
         panic(err) 
   } 

   buf := make([]byte, BUFFERSIZE) 
   for { 
         n, err := source.Read(buf) 
         if err != nil && err != io.EOF { 
               return err 
         } 
         if n == 0 { 
               break 
         } 

         if _, err := destination.Write(buf[:n]); err != nil { 
               return err 
         } 
   } 
   return err 
} 

这里没有什么特别之处:您只需继续调用 source,Read(),直到到达输入文件的末尾。每次你读到什么东西,你就叫目的地。Write()保存到输出文件。buf[:n]符号允许您读取buf片段中的第一个n字符。

第四部分包含以下 Go 代码:

func main() { 
   if len(os.Args) != 4 { 
         fmt.Printf("usage: %s source destination BUFFERSIZE\n", 
filepath.Base(os.Args[0])) 
         os.Exit(1) 
   } 

   source := os.Args[1] 
   destination := os.Args[2] 
   BUFFERSIZE, _ = strconv.ParseInt(os.Args[3], 10, 64) 

filepath.Base()用于获取可执行文件的名称。

最后一部分是以下内容:

   fmt.Printf("Copying %s to %s\n", source, destination) 
   err := Copy(source, destination, BUFFERSIZE) 
   if err != nil { 
         fmt.Printf("File copying failed: %q\n", err) 
   } 
}

执行cp.go将生成以下输出:

$ go run cp.go inputFile aCopy 2048
Copying inputFile to aCopy
$ diff inputFile aCopy

如果copy操作有问题,您将收到一条描述性错误消息。

因此,如果程序找不到输入文件,它将打印以下内容:

$ go run cp.go A /tmp/myCP 1024
Copying A to /tmp/myCP
File copying failed: "stat A: no such file or directory"

如果程序无法读取输入文件,您将收到以下消息:

$ go run cp.go inputFile /tmp/myCP 1024
Copying inputFile to /tmp/myCP
File copying failed: "open inputFile: permission denied"

如果程序无法创建输出文件,它将打印以下错误消息:

$ go run cp.go inputFile /usr/myCP 1024
Copying inputFile to /usr/myCP
File copying failed: "open /usr/myCP: operation not permitted"

如果目标文件已存在,您将获得以下输出:

$ go run cp.go inputFile outputFile 1024
Copying inputFile to outputFile
File copying failed: "File outputFile already exists."

基准文件复制操作

文件操作中使用的缓冲区的大小非常重要,它会影响系统工具的性能,尤其是在处理非常大的文件时。

虽然开发可靠的软件应该是您的主要关注点,但您不应该忘记让您的系统软件快速高效!

因此,本节将尝试通过使用不同的缓冲区大小执行cp.go,并将其性能与readAll.gonotGoodCP.go以及cp(1)进行比较,来了解缓冲区大小如何影响文件复制操作。

在旧的 Unix 时代,当 Unix 机器上的 RAM 太少时,不建议使用大的缓冲区。然而,如今,使用大小为100 MB的缓冲区并不被认为是不好的做法,尤其是当您事先知道要复制大量大文件(如数据库服务器的数据文件)时。

我们将在测试中使用三个大小不同的文件:这三个文件将使用dd(1)实用程序生成,如下所示:

$dd if=/dev/urandom of=100MB count=100000 bs=1024
100000+0 records in
100000+0 records out
102400000 bytes transferred in 6.800277 secs (15058210 bytes/sec)
$ dd if=/dev/urandom of=1GB count=1000000 bs=1024
1000000+0 records in
1000000+0 records out
1024000000 bytes transferred in 68.887482 secs (14864820 bytes/sec)
$ dd if=/dev/urandom of=5GB count=5000000 bs=1024
5000000+0 records in
5000000+0 records out
5120000000 bytes transferred in 339.357738 secs (15087324 bytes/sec)
$ ls -l 100MB 1GB 5GB
-rw-r--r--  1 mtsouk  staff   102400000 May  4 10:30 100MB
-rw-r--r--  1 mtsouk  staff  1024000000 May  4 10:32 1GB
-rw-r--r--  1 mtsouk  staff  5120000000 May  4 10:38 5GB

第一个文件为100 MB,第二个文件为1 GB,第三个文件为5 GB

现在,是使用time(1)实用程序进行实际测试的时候了。首先,我们将测试notGoodCP.goreadAll.go的性能:

$ time ./notGoodCP 100MB copy
Copied 102400000 bytes!

real  0m0.153s
user  0m0.003s
sys   0m0.084s
$ time ./notGoodCP 1GB copy
Copied 1024000000 bytes!

real  0m1.461s
user  0m0.029s
sys   0m0.833s
$ time ./notGoodCP 5GB copy
Copied 5120000000 bytes!

real  0m12.193s
user  0m0.161s
sys   0m5.251s
$ time ./readAll 100MB copy

real  0m0.249s
user  0m0.003s
sys   0m0.138s
$ time ./readAll 1GB copy

real  0m3.117s
user  0m0.639s
sys   0m1.644s
$ time ./readAll 5GB copy

real  0m28.918s
user  0m8.106s
sys   0m21.364s

现在,您将看到使用四种不同缓冲区大小的cp.go程序的结果,16102410485761073741824。首先,我们复制100 MB文件:

$ time ./cp 100MB copy 16
Copying 100MB to copy

real  0m13.240s
user  0m2.699s
sys   0m10.530s
$ time ./cp 100MB copy 1024
Copying 100MB to copy

real  0m0.386s
user  0m0.053s
sys   0m0.303s
$ time ./cp 100MB copy 1048576
Copying 100MB to copy

real  0m0.135s
user  0m0.001s
sys   0m0.075s
$ time ./cp 100MB copy 1073741824
Copying 100MB to copy

real  0m0.390s
user  0m0.011s
sys   0m0.136s

然后,我们将复制1 GB文件:

$ time ./cp 1GB copy 16
Copying 1GB to copy

real  2m10.054s
user  0m26.497s
sys   1m43.411s
$ time ./cp 1GB copy 1024
Copying 1GB to copy

real  0m3.520s
user  0m0.533s
sys   0m2.944s
$ time ./cp 1GB copy 1048576
Copying 1GB to copy

real  0m1.431s
user  0m0.006s
sys   0m0.749s
$ time ./cp 1GB copy 1073741824
Copying 1GB to copy

real  0m2.033s
user  0m0.012s
sys   0m1.310s

接下来,我们将复制5 GB文件:

$ time ./cp 5GB copy 16
Copying 5GB to copy

real  10m41.551s
user  2m11.695s
sys   8m29.248s
$ time ./cp 5GB copy 1024
Copying 5GB to copy

real  0m16.558s
user  0m2.415s
sys   0m13.597s
$ time ./cp 5GB copy 1048576
Copying 5GB to copy

real  0m7.172s
user  0m0.028s
sys   0m3.734s
$ time ./cp 5GB copy 1073741824
Copying 5GB to copy

real  0m8.612s
user  0m0.011s
sys   0m4.536s

最后,让我们展示 macOS Sierra 附带的cp(1)实用程序的结果:

$ time cp 100MB copy

real  0m0.274s
user  0m0.002s
sys   0m0.105s
$ time cp 1GB copy

real  0m2.735s
user  0m0.003s
sys   0m1.014s
$ time cp 5GB copy

real  0m12.199s
user  0m0.012s
sys   0m5.050s

下图显示了一个图表,其中包含time(1)实用程序输出的所有上述结果的实字段值:

各种拷贝实用程序的基准测试结果

从结果中可以看出,cp(1)实用程序做得相当好。但是,cp.go更通用,因为它允许您定义缓冲区的大小。另一方面,如果您使用具有较小缓冲区大小(16 字节)的cp.go,那么整个过程将完全被破坏!另外,有趣的是,To.T3AL 在相对较小的文件上做了相当不错的工作,而且只有在复制 ORT T4 文件时才是慢的,这对于这样一个小程序来说并不坏:你可以把 ALE T5 作为一个快速和肮脏的解决方案!

Go 中 wc(1)的研制

wc.go程序代码背后的主要思想是,您可以逐行读取文本文件,直到没有任何内容可读取为止。对于您阅读的每一行,您都可以找出它的字符数和字数。由于您需要逐行读取输入,因此最好使用bufio而不是普通的io,因为它简化了代码。然而,尝试使用io自行实现wc.go将是一个非常有教育意义的练习。

但首先,您将看到wc(1)实用程序生成以下输出:

$ wc wc.go cp.go
      68     160    1231 wc.go
      45     112     755 cp.go
     113     272    1986 total

因此,如果wc(1)必须处理多个文件,它会自动生成摘要信息。

第 9 章Goroutines-基本功能中,您将学习如何使用 Go 例程创建wc.go版本。但是,两个版本的核心功能将完全相同!

数词

代码实现中最棘手的部分是字数计算,它是使用正则表达式实现的:

r := regexp.MustCompile("[^\\s]+") 
for range r.FindAllString(line, -1) { 
numberOfWords++ 
} 

这里,所提供的正则表达式基于空格字符分隔一行中的单词,以便随后对它们进行计数!

wc.go 代码!

在这篇简短的介绍之后,是时候看看wc.go的 Go 代码了,它将分为五个部分。第一部分是预期的序言:

package main 

import ( 
   "bufio" 
   "flag" 
   "fmt" 
   "io" 
   "os" 
   "regexp" 
) 

第二部分是countLines()功能的实现,包括程序的核心功能。请注意,名称countLines()可能是一个糟糕的选择,因为countLines()还计算文件的单词和字符:

func countLines(filename string) (int, int, int) { 
   var err error 
   var numberOfLines int 
   var numberOfCharacters int 
   var numberOfWords int 
   numberOfLines = 0

   numberOfCharacters = 0 
   numberOfWords = 0 

   f, err := os.Open(filename) 
   if err != nil { 
         fmt.Printf("error opening file %s", err) 
         os.Exit(1) 
   } 
   defer f.Close() 

   r := bufio.NewReader(f) 
   for { 
         line, err := r.ReadString('\n') 

         if err == io.EOF { 
               break 
         } else if err != nil { 
               fmt.Printf("error reading file %s", err)
                break 
         } 

         numberOfLines++ 
         r := regexp.MustCompile("[^\\s]+") 
         for range r.FindAllString(line, -1) { 
               numberOfWords++ 
         } 
         numberOfCharacters += len(line) 
   } 

   return numberOfLines, numberOfWords, numberOfCharacters 
} 

这里有很多有趣的东西。首先,您可以看到上一节中介绍的用于计算每行字数的 Go 代码。计算行数很容易,因为每次bufio读取器读取新行时,numberOfLines变量的值都会增加 1。ReadString()函数告诉程序读取直到输入中第一次出现'\n':多次调用ReadString()意味着您正在逐行读取文件。

接下来,您可以看到countLines()函数返回三个整数值。最后,通过len()函数实现字符计数,该函数返回给定字符串中的字符数,在本例中是读取的行。当您收到io.EOF错误消息时,for循环终止,这表示输入文件中没有任何内容可供读取。

wc.go的第三部分从main()功能开始执行开始,也包括flag包的配置:

func main() { 
   minusC := flag.Bool("c", false, "Characters") 
   minusW := flag.Bool("w", false, "Words") 
   minusL := flag.Bool("l", false, "Lines") 

   flag.Parse() 
   flags := flag.Args() 

   if len(flags) == 0 { 
         fmt.Printf("usage: wc <file1> [<file2> [... <fileN]]\n") 
         os.Exit(1) 
   } 

   totalLines := 0 
   totalWords := 0 
   totalCharacters := 0 
   printAll := false 

   for _, filename := range flag.Args() { 

最后一条for语句用于处理提供给程序的所有输入文件。wc.go程序支持三个标志:-c标志用于打印字符数,-w标志用于打印字数,-l标志用于打印行数。

第四部分是以下内容:

         numberOfLines, numberOfWords, numberOfCharacters := countLines(filename) 

         totalLines = totalLines + numberOfLines 
         totalWords = totalWords + numberOfWords 
         totalCharacters = totalCharacters + numberOfCharacters 

         if (*minusC && *minusW && *minusL) || (!*minusC && !*minusW && !*minusL) { 
               fmt.Printf("%d", numberOfLines) 
               fmt.Printf("\t%d", numberOfWords) 
               fmt.Printf("\t%d", numberOfCharacters) 
               fmt.Printf("\t%s\n", filename) 
               printAll = true 
               continue 
         } 

         if *minusL { 
               fmt.Printf("%d", numberOfLines) 
         } 

         if *minusW { 
               fmt.Printf("\t%d", numberOfWords) 
         } 

         if *minusC { 
               fmt.Printf("\t%d", numberOfCharacters) 
         } 

         fmt.Printf("\t%s\n", filename) 
   } 

本部分将根据命令行标志按文件打印信息。如您所见,这里的大多数 Go 代码都是用于根据命令行标志处理输出的。

最后一部分是以下内容:

   if (len(flags) != 1) && printAll { 
         fmt.Printf("%d", totalLines) 
         fmt.Printf("\t%d", totalWords) 
         fmt.Printf("\t%d", totalCharacters) 
         fmt.Println("\ttotal") 
return 
   } 

   if (len(flags) != 1) && *minusL { 
         fmt.Printf("%d", totalLines) 
   } 

   if (len(flags) != 1) && *minusW { 
         fmt.Printf("\t%d", totalWords) 
   } 

   if (len(flags) != 1) && *minusC { 
         fmt.Printf("\t%d", totalCharacters) 
   } 

   if len(flags) != 1 { 
         fmt.Printf("\ttotal\n") 
   } 
} 

在这里,您可以打印根据程序标志读取的行、字和字符总数。同样,这里的大多数 Go 代码都是用于根据命令行标志修改输出的。

执行wc.go将生成以下输出:

$ go build wc.go
$ ls -l wc
-rwxr-xr-x  1 mtsouk  staff  2264384 Apr 29 21:10 wc
$ ./wc wc.go sparse.go notGoodCP.go
120   280   2319  wc.go
44    98    697   sparse.go
27    61    418   notGoodCP.go
191   439   3434  total
$ ./wc -l wc.go sparse.go
120   wc.go
44    sparse.go
164   total
$ ./wc -w -l wc.go sparse.go
120   280   wc.go
44    98    sparse.go
164   378   total

这里有一个微妙之处:使用 Go 源文件作为go run wc.go命令的命令行参数将失败。这将发生,因为编译器将尝试编译 Go 源文件,而不是将它们视为go run wc.go命令的命令行参数。以下输出证明了这一点:

$ go run wc.go sparse.go
# command-line-arguments
./sparse.go:11: main redeclared in this block
      previous declaration at ./wc.go:49
$ go run wc.go wc.go
package main: case-insensitive file name collision:
"wc.go" and "wc.go"
$ go run wc.go cp.go sparse.go
# command-line-arguments
./cp.go:35: main redeclared in this block
      previous declaration at ./wc.go:49
./sparse.go:11: main redeclared in this block
      previous declaration at ./cp.go:35

此外,尝试在 Go 版本为 1.3.3 的 Linux 系统上执行wc.go将失败,并显示以下错误消息:

$ go version
go version go1.3.3 linux/amd64
$ go run wc.go
# command-line-arguments
./wc.go:40: syntax error: unexpected range, expecting {
./wc.go:46: non-declaration statement outside function body
./wc.go:47: syntax error: unexpected }

比较 wc.go 和 wc(1)的性能

在本小节中,我们将比较我们的wc(1)版本与 macOS Sierra 10.12.6 附带的wc(1)版本的性能。首先,我们将执行wc.go

$ file wc
wc: Mach-O 64-bit executable x86_64
$ time ./wc *.data
672320      3361604     9413057     connections.data
269123      807369      4157790     diskSpace.data
672040      1344080     8376070     memory.data
1344533     2689066     5378132     pageFaults.data
269465      792715      4068250     uptime.data
3227481     8994834     31393299    total

real  0m17.467s
user  0m22.164s
sys   0m3.885s

然后,我们将执行 macOS 版本的wc(1)来处理相同的文件:

$ file `which wc`
/usr/bin/wc: Mach-O 64-bit executable x86_64
$ time wc *.data
672320 3361604 9413057 connections.data
269123  807369 4157790 diskSpace.data
672040 1344080 8376070 memory.data
1344533 2689066 5378132 pageFaults.data
269465  792715 4068250 uptime.data
3227481 8994834 31393299 total

real  0m0.086s
user  0m0.076s
sys   0m0.007s

先来看好消息,;这两个实用程序生成完全相同的输出,这意味着我们的 Go 版本的wc(1)工作得非常好,可以处理大文本文件!

现在,坏消息;wc.go太慢了!处理所有五个文件花费了wc(1)不到一秒的时间,而执行相同的任务却花费了wc.go将近 18 秒的时间!

在任何平台上使用任何编程语言开发任何类型的软件时,一般的想法是,在尝试优化之前,您应该尝试使用一个不包含任何错误的工作版本,而不是相反!

逐字读取文本文件

虽然wc(1)实用程序的开发不需要逐字读取文本文件,但最好知道如何在 Go 中实现它。文件名为charByChar.go,分为四个部分。

第一部分是以下 Go 代码:

package main 

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

尽管charByChar.go没有很多行 Go 代码,但它需要很多 Go 标准包,这是一个天真的迹象,表明它实现的任务并不平凡。第二部分内容如下:

func main() { 
   arguments := os.Args 
   if len(arguments) == 1 { 
         fmt.Println("Not enough arguments!") 
         os.Exit(1) 
   } 
   input := arguments[1] 

第三部分如下:

   buf, err := ioutil.ReadFile(input) 
   if err != nil { 
         fmt.Println(err) 
         os.Exit(1) 
   } 

最后一部分具有以下 Go 代码:

   in := string(buf) 
   s := bufio.NewScanner(strings.NewReader(in)) 
   s.Split(bufio.ScanRunes) 

   for s.Scan() { 
         fmt.Print(s.Text()) 
   } 
} 

这里,ScanRunes是一个分割函数,返回每个字符(符文)作为标记。然后,对Scan()的调用允许我们逐个处理每个字符。还有分别用于获取单词和行的ScanWordsScanLines。如果您使用fmt.Println(s.Text())作为程序中的最后一条语句而不是fmt.Print(s.Text()),则每个字符将打印在自己的行上,程序的任务将更加明显。

执行charByChar.go生成以下输出:

$ go run charByChar.go test
package main
...

wc(1)命令可以通过比较输入文件和charByChar.go生成的输出来验证charByChar.go的 Go 代码的正确性:

$ go run charByChar.go test | wc
      32      54     439
$ wc test
      32      54     439 test

做一些文件编辑!

本节将介绍一个 Go 程序,该程序可将文件中的制表符转换为空格字符,反之亦然!这是一项通常由文本编辑器完成的工作,但最好知道如何自己执行。

代码将保存在tabSpace.go中,并分四个部分呈现。

注意,tabSpace.go逐行读取文本文件,但您也可以开发一个逐字读取文本文件的版本。

在当前的实现中,所有的工作都是在正则表达式、模式匹配以及搜索和替换操作的帮助下完成的。

第一部分是预期的序言:

package main 

import ( 
   "bufio" 
   "fmt" 
   "io" 
   "os" 
   "path/filepath" 
   "strings" 
) 

第二部分包含以下 Go 代码:

func main() { 
   if len(os.Args) != 3 { 
         fmt.Printf("Usage: %s [-t|-s] filename!\n", filepath.Base(os.Args[0])) 
         os.Exit(1) 
   } 
   convertTabs := false 
   convertSpaces := false 
   newLine := "" 

   option := os.Args[1] 
   filename := os.Args[2] 
   if option == "-t" { 
         convertTabs = true 
   } else if option == "-s" { 
         convertSpaces = true 
   } else { 
         fmt.Println("Unknown option!") 
         os.Exit(1) 
   } 

第三部分包含以下 Go 代码:

   f, err := os.Open(filename) 
   if err != nil { 
         fmt.Printf("error opening %s: %s", filename, err) 
         os.Exit(1) 
   } 
   defer f.Close() 

   r := bufio.NewReader(f) 
   for { 
         line, err := r.ReadString('\n') 

         if err == io.EOF { 
               break 
         } else if err != nil { 
               fmt.Printf("error reading file %s", err) 
               os.Exit(1) 
         } 

最后一部分是以下内容:

         if convertTabs == true { 
               newLine = strings.Replace(line, "\t", "    ", -1) 
         } else if convertSpaces == true { 
               newLine = strings.Replace(line, "    ", "\t", -1) 
         } 

         fmt.Print(newLine) 
   } 
} 

这一部分是使用适当的strings.Replace()调用实现魔法的地方。在其当前的实现中,每个选项卡都被四个空格字符替换,反之亦然,但您可以通过修改 Go 代码来更改它。

再次强调,tabSpace.go的很大一部分与错误处理有关,因为当您试图打开文件进行读取时,可能会发生许多奇怪的事情!

根据 Unix 原理,tabSpace.go的输出将打印在屏幕上,而不会保存在新的文本文件中。将tabSpace.gowc(1)一起使用可以证明其正确性:

$ go run tabSpace.go -t cp.go > convert
$ wc convert cp.go
    76     192    1517 convert 
      76     192    1286 cp.go
     152     384    2803 total
$ go run tabSpace.go -s convert | wc
      76     192    1286

进程间通信

进程间通信IPC),简单地说就是允许 Unix 进程相互通信。存在允许进程和程序相互对话的各种技术。Unix 系统中最常用的一种技术是管道,它从早期 Unix 时代就存在了。章节8流程和信号将详细介绍如何在 Go 中实现 Unix 管道。IPC 的另一种形式是 Unix 域套接字,这也将在章节8进程和信号中讨论。

章节12网络编程将讨论另一种进程间通信形式,即网络套接字。共享内存也存在,但 Go 反对将共享内存用作通信手段。9 章Goroutines-基本功能第 10 章Goroutines-高级功能将展示允许 Goroutines 与他人通信、共享和交换数据的各种技术。

Go中的稀疏文件

使用os.Seek()功能创建的大文件可能有洞,占用的磁盘块比大小相同但没有洞的文件少;这样的文件称为稀疏文件。本节将开发一个创建稀疏文件的程序。

sparse.go的 Go 代码将分三部分介绍。第一部分如下:

package main 

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

sparse.go的第二部分有以下 Go 代码:

func main() { 
   if len(os.Args) != 3 { 
         fmt.Printf("usage: %s SIZE filename\n", filepath.Base(os.Args[0])) 
         os.Exit(1) 
   } 

   SIZE, _ := strconv.ParseInt(os.Args[1], 10, 64) 
   filename := os.Args[2] 

   _, err := os.Stat(filename) 
   if err == nil { 
         fmt.Printf("File %s already exists.\n", filename) 
         os.Exit(1) 
   } 

strconv.ParseInt()函数用于将定义稀疏文件大小的命令行参数从字符串值转换为整数值。此外,os.Stat()调用确保不会意外覆盖现有文件。

最后一部分是行动发生的地方:

   fd, err := os.Create(filename) 
   if err != nil { 
         log.Fatal("Failed to create output") 
   } 

   _, err = fd.Seek(SIZE-1, 0) 
   if err != nil { 
         fmt.Println(err) 
         log.Fatal("Failed to seek") 
   } 

   _, err = fd.Write([]byte{0}) 
   if err != nil { 
         fmt.Println(err) 
         log.Fatal("Write operation failed") 
   } 

   err = fd.Close() 
   if err != nil { 
         fmt.Println(err) 
         log.Fatal("Failed to close file") 
   } 
} 

首先,您尝试使用os.Create()创建所需的稀疏文件。然后,您调用fd.Seek()以便在不添加实际数据的情况下使文件变大。最后,使用fd.Write()向其写入一个字节。由于您对该文件没有更多的处理,因此您可以调用fd.Close()并完成操作。

执行sparse.go生成以下输出:

$ go run sparse.go 1000 test
$ go run sparse.go 1000 test
File test already exists.
exit status 1

如何判断文件是否为稀疏文件?稍后您将了解这一点,但首先,让我们创建一些文件:

$ go run sparse.go 100000 testSparse $ dd if=/dev/urandom  bs=1 count=100000 of=noSparseDD
100000+0 records in
100000+0 records out
100000 bytes (100 kB) copied, 0.152511 s, 656 kB/s
$ dd if=/dev/urandom seek=100000 bs=1 count=0 of=sparseDD
0+0 records in
0+0 records out
0 bytes (0 B) copied, 0.000159399 s, 0.0 kB/s
$ ls -l noSparseDD sparseDD testSparse
-rw-r--r-- 1 mtsouk mtsouk 100000 Apr 29 21:43 noSparseDD
-rw-r--r-- 1 mtsouk mtsouk 100000 Apr 29 21:43 sparseDD
-rw-r--r-- 1 mtsouk mtsouk 100000 Apr 29 21:40 testSparse

请注意,某些 Unix 变体不会创建稀疏文件:首先想到的此类 Unix 变体是使用 HFS 文件系统的 macOS。因此,为了获得更好的结果,您可以在 Linux 机器上执行所有这些命令。

那么,如何判断这三个文件中是否有一个是稀疏文件?ls(1)实用程序的-s标志显示文件实际使用的文件系统块的数量。因此,ls -ls命令的输出允许您检测是否正在处理稀疏文件:

$ ls -ls noSparseDD sparseDD testSparse
104 -rw-r--r-- 1 mtsouk mtsouk 100000 Apr 29 21:43 noSparseDD
      0 -rw-r--r-- 1 mtsouk mtsouk 100000 Apr 29 21:43 sparseDD
      8 -rw-r--r-- 1 mtsouk mtsouk 100000 Apr 29 21:40 testSparse

现在看输出的第一列。使用dd(1)实用程序生成的noSparseDD文件不是稀疏文件。sparseDD文件是使用dd(1)实用程序生成的稀疏文件。最后,testSparse也是使用sparse.go创建的稀疏文件。

读写数据记录

本节将教您如何处理数据记录的写入和读取。记录与其他类型的文本数据的区别在于,记录具有特定数量的字段的给定结构:将其视为关系数据库中表中的一行。事实上,如果您想在 Go 中开发自己的数据库服务器,记录对于在表中存储数据非常有用!

records.go的 Go 代码将以 CSV 格式保存数据,并将分四部分呈现。第一部分包含以下 Go 代码:

package main 

import ( 
   "encoding/csv" 
   "fmt" 
   "os" 
) 

因此,在这里您必须声明您将以 CSV 格式读取或写入数据。第二部分如下:

func main() { 
   if len(os.Args) != 2 { 
         fmt.Println("Need just one filename!") 
         os.Exit(-1) 
   } 

   filename := os.Args[1] 
   _, err := os.Stat(filename) 
   if err == nil { 
         fmt.Printf("File %s already exists.\n", filename) 
         os.Exit(1) 
   } 

计划的第三部分如下:

   output, err := os.Create(filename) 
   if err != nil { 
         fmt.Println(err) 
         os.Exit(-1) 
   } 
   defer output.Close() 

   inputData := [][]string{{"M", "T", "I."}, {"D", "T", "I."}, 
{"M", "T", "D."}, {"V", "T", "D."}, {"A", "T", "D."}} 
   writer := csv.NewWriter(output) 
   for _, record := range inputData { 
         err := writer.Write(record) 
         if err != nil { 
               fmt.Println(err) 
               os.Exit(-1) 
         } 
   } 
   writer.Flush() 

您应熟悉本部分的操作;到目前为止,与您在本章中看到的最大区别在于作者来自csv软件包。

records.go的最后一部分有以下 Go 代码:

   f, err := os.Open(filename) 
   if err != nil { 
         fmt.Println(err) 
         return 
   } 
   defer f.Close() 

   reader := csv.NewReader(f) 
   reader.FieldsPerRecord = -1 
   allRecords, err := reader.ReadAll() 
   if err != nil { 
         fmt.Println(err) 
         os.Exit(1) 
   } 

   for _, rec := range allRecords { 
         fmt.Printf("%s:%s:%s\n", rec[0], rec[1], rec[2]) 
   } 
} 

reader一次读取整个文件,使整个操作更快。但是,如果您处理的是巨大的数据文件,则每次可能需要读取文件的较小部分,直到读取完整的文件为止。使用的reader来自csv包装。

执行records.go将创建以下输出:

$ go run records.go recordsDataFile
M:T:I. 
D:T:I.
M:T:D.
V:T:D.
A:T:D.
$ ls -l recordsDataFile
-rw-r--r--  1 mtsouk  staff  35 May  2 19:20 recordsDataFile

名为recordsDataFile的 CSV 文件包含以下数据:

$ cat recordsDataFile
M,T,I.
D,T,I.
M,T,D.
V,T,D.
A,T,D.

Go中的文件锁定

有时,您不希望同一进程的任何其他子进程更改文件,甚至不希望访问该文件,因为您正在更改其数据,并且您不希望其他进程读取不完整或不一致的数据。虽然您将在第 9 章Goroutines-基本功能第 10 章Goroutines-高级功能中了解更多关于文件锁定和 go 例程的信息,本章将给出一个小的 Go 示例,不作详细解释,以便让您了解事物的工作原理:您应该等到第 9 章Goroutines-基本功能第 10 章Goroutines-高级功能了解更多信息。

所提出的技术将使用Mutex,这是一种通用的同步机制。Mutex锁允许我们在同一个 Go 进程中锁定文件。因此,这种技术与flock(2)系统调用的使用无关。

存在各种文件锁定技术。其中之一是创建一个附加文件,表示另一个程序或进程正在使用给定的资源。所提出的技术更适合于使用多个 go 例程的程序。

用于写入的文件锁定技术将在fileLocking.go中说明,该技术将分为四个部分。第一部分如下:

package main 

import ( 
   "fmt" 
   "math/rand" 
   "os" 
   "sync" 
   "time" 
) 

var mu sync.Mutex 

func random(min, max int) int { 
   return rand.Intn(max-min) + min 
} 

第二部分如下:

func writeDataToFile(i int, file *os.File, w *sync.WaitGroup) { 
   mu.Lock() 
   time.Sleep(time.Duration(random(10, 1000)) * time.Millisecond) 
   fmt.Fprintf(file, "From %d, writing %d\n", i, 2*i) 
   fmt.Printf("Wrote from %d\n", i) 
   w.Done() 
mu.Unlock() 
} 

使用mu.Lock()语句锁定文件,使用mu.Unlock()语句解锁文件。

第三部分如下:

func main() { 
   if len(os.Args) != 2 { 
         fmt.Println("Please provide one command line argument!") 
         os.Exit(-1) 
   } 

   filename := os.Args[1] 
   number := 3 

   file, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) 
   if err != nil { 
         fmt.Println(err) 
         os.Exit(1) 
   } 

最后一部分是以下 Go 代码:

   var w *sync.WaitGroup = new(sync.WaitGroup) 
   w.Add(number) 

   for r := 0; r < number; r++ { 
         go writeDataToFile(r, file, w) 
   } 

   w.Wait() 
} 

执行fileLocking.go将创建以下输出:

$ go run fileLocking.go 123
Wrote from 0
Wrote from 2
Wrote from 1
$ cat /tmp/swtag.log
From 0, writing 0
From 2, writing 4
From 1, writing 2

正确版本的fileLocking.gowriteDataToFile()函数的末尾调用了mu.Unlock(),允许所有 goroutine 使用该文件。如果您从writeDataToFile()函数中删除对mu.Unlock()的调用,并执行fileLocking.go,您将获得以下输出:

$ go run fileLocking.go 123
Wrote from 2
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [semacquire]:
sync.runtime_Semacquire(0xc42001024c)
      /usr/local/Cellar/go/1.8.1/libexec/src/runtime/sema.go:47 +0x34
sync.(*WaitGroup).Wait(0xc420010240)
      /usr/local/Cellar/go/1.8.1/libexec/src/sync/waitgroup.go:131 +0x7a
main.main()
     /Users/mtsouk/Desktop/goBook/ch/ch6/code/fileLocking.go:47 +0x172

goroutine 5 [semacquire]:
sync.runtime_SemacquireMutex(0x112dcbc)
     /usr/local/Cellar/go/1.8.1/libexec/src/runtime/sema.go:62 +0x34
sync.(*Mutex).Lock(0x112dcb8)
      /usr/local/Cellar/go/1.8.1/libexec/src/sync/mutex.go:87 +0x9d
main.writeDataToFile(0x0, 0xc42000c028, 0xc420010240)
      /Users/mtsouk/Desktop/goBook/ch/ch6/code/fileLocking.go:18 +0x3f
created by main.main
      /Users/mtsouk/Desktop/goBook/ch/ch6/code/fileLocking.go:44 +0x151

goroutine 6 [semacquire]:
sync.runtime_SemacquireMutex(0x112dcbc)
      /usr/local/Cellar/go/1.8.1/libexec/src/runtime/sema.go:62 +0x34
sync.(*Mutex).Lock(0x112dcb8)
      /usr/local/Cellar/go/1.8.1/libexec/src/sync/mutex.go:87 +0x9d
main.writeDataToFile(0x1, 0xc42000c028, 0xc420010240)
      /Users/mtsouk/Desktop/goBook/ch/ch6/code/fileLocking.go:18 +0x3f
created by main.main
    /Users/mtsouk/Desktop/goBook/ch/ch6/code/fileLocking.go:44 +0x151 exit status 2
$ cat 123
From 2, writing 4

获取此输出的原因是,除了第一个 goroutine 能够执行mu.Lock()语句外,其余 goroutine 无法获取Mutex。因此,他们无法写入文件,这意味着他们将永远无法完成工作并等待,这就是 Go 生成上述错误消息的原因。

如果您不完全理解此示例,您应该等到第 9 章Goroutines-基本功能第 10 章Goroutines-高级功能之后。

dd 实用程序的简化 Go 版本

dd(1)工具可以做很多事情,但本节将实现其功能的一小部分。我们版本的dd(1)将支持两个命令行标志:一个用于指定以字节为单位的块大小(-bs),另一个用于指定将写入的块总数(-count。将这两个值相乘将得到生成文件的大小(以字节为单位)。

Go 代码保存为ddGo.go,将分四部分呈现给您。第一部分是预期的序言:

package main 

import ( 
   "flag" 
   "fmt" 
   "math/rand" 
   "os" 
   "time" 
) 

第二部分包含两个函数的 Go 代码:

func random(min, max int) int { 
   return rand.Intn(max-min) + min 
} 

func createBytes(buf *[]byte, count int) { 
   if count == 0 { 
         return 
   } 
   for i := 0; i < count; i++ { 
         intByte := byte(random(0, 9)) 
         *buf = append(*buf, intByte) 
   } 
} 

第一个函数用于获取随机数,第二个函数用于创建一个字节片,其中所需大小填充了随机数。

ddGo.go的第三部分如下:

func main() { 
   minusBS := flag.Int("bs", 0, "Block Size") 
   minusCOUNT := flag.Int("count", 0, "Counter") 
   flag.Parse() 
   flags := flag.Args() 

   if len(flags) == 0 { 
         fmt.Println("Not enough arguments!") 
         os.Exit(-1) 
   } 

   if *minusBS < 0 || *minusCOUNT < 0 { 
         fmt.Println("Count or/and Byte Size < 0!") 
         os.Exit(-1) 
   } 

   filename := flags[0] 
   rand.Seed(time.Now().Unix()) 

   _, err := os.Stat(filename) 
   if err == nil { 
         fmt.Printf("File %s already exists.\n", filename) 
         os.Exit(1) 
   } 

   destination, err := os.Create(filename) 
   if err != nil { 
         fmt.Println("os.Create:", err) 
         os.Exit(1) 
   } 

这里,您主要处理程序的命令行参数。

最后一部分是以下内容:

   buf := make([]byte, *minusBS) 
   buf = nil 
   for i := 0; i < *minusCOUNT; i++ { 
         createBytes(&buf, *minusBS) 
         if _, err := destination.Write(buf); err != nil { 
               fmt.Println(err) 
               os.Exit(-1) 
         } 
         buf = nil 
   } 
} 

每次调用createBytes()时清空buf字节片的原因是您不希望每次调用createBytes()函数时buf字节片变得越来越大。这是因为append()函数在片的末尾添加数据,而不触及现有数据。

在我编写的第一版ddGo.go中,我忘记了在每次调用createBytes()之前清空buf字节片。因此,生成的文件比预期的大!我花了一段时间和几句fmt.Println(buf)的话才找到这种不可预见的行为的原因!

ddGo.go的执行会很快生成您想要的文件:

$ time go run ddGo.go -bs=10000 -count=5000 test3

real  0m1.655s
user  0m1.576s
sys   0m0.104s
$ ls -l test3
-rw-r--r--  1 mtsouk  staff  50000000 May  6 15:27 test3

此外,使用随机数会使生成的相同大小的文件彼此不同:

$ go run ddGo.go -bs=100 -count=50 test1
$ go run ddGo.go -bs=100 -count=50 test2
$ ls -l test1 test2
-rw-r--r--  1 mtsouk  staff  5000 May  6 15:26 test1
-rw-r--r--  1 mtsouk  staff  5000 May  6 15:26 test2
$ diff test1 test2
Binary files test1 and test2 differ

练习

  1. 访问bufio包的文档页面,可在找到 https://golang.org/pkg/bufio/
  2. 请访问上的io包文件 https://golang.org/pkg/io/
  3. 试着加快速度。
  4. 实现tabSpace.go的功能,但尝试逐字读取输入文本文件,而不是逐行读取。
  5. 更改代码tabSpace.go,以便能够获取将替换制表符作为命令行参数的空格数。
  6. 了解有关小端和大端表示的更多信息。

总结

在本章中,我们讨论了 Go 中的文件输入和输出。除此之外,我们还开发了wc(1)dd(1)cp(1)Unix 命令行实用程序的 Go 版本,同时进一步了解了 Go 标准库的iobufio包,这些包允许您读取和写入文件。

在下一章中,我们将讨论另一个重要主题,即如何处理 Unix 机器的系统文件。此外,您还将学习如何读取和更改 Unix 文件权限,以及如何查找文件的所有者和组。此外,我们还将讨论日志文件以及如何使用模式匹配从日志文件中获取所需信息。