Skip to content

Latest commit

 

History

History
1168 lines (892 loc) · 40.7 KB

File metadata and controls

1168 lines (892 loc) · 40.7 KB

十、Go 中的数据 IO

本书的前几章主要关注基础知识。在本章和以后的章节中,读者将了解 Go 的标准库提供的一些功能强大的 API。本章详细讨论了如何使用标准库及其各自包中的 API 输入、处理、转换和输出数据,主题如下:

  • 与读者和作家的对话
  • io.Reader 接口
  • io.Writer 接口
  • 使用 io 包
  • 使用文件
  • 用 fmt 格式化 IO
  • 缓冲 IO
  • 内存 IO
  • 数据编解码

与读者和作者交流

与 Java 等其他语言类似,Go 将数据输入和输出建模为从源流向目标的流。数据资源,例如文件、网络连接,甚至一些内存对象,可以建模为字节流,从中可以读取写入,如下图所示:

IO with readers and writers

数据流表示为可被访问以进行读取或写入的字节(【】字节)的片。正如我们将在本章中探讨的, *io* 包提供io.Reader接口,以实现读取并将数据从源传输到字节流的代码。相反,io.Writer接口允许实现者创建代码,从提供的字节流读取数据,作为输出写入目标资源。这两个接口在 Go 中被广泛用作表示 IO 操作的标准习惯用法。这使得可以交换具有可预测结果的不同实现和上下文的读者和作者。

io.读卡器接口

io.Reader接口很简单,如下面的清单所示。它由一个方法Read([]byte)(int, error)组成,旨在让程序员实现从任意源读取数据的代码,并将其传输到提供的字节片中。

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

Read方法返回传输到所提供片的字节总数和错误值(如果需要)。作为指导原则,io.Reader的实现应该在读取器没有更多数据传输到流p时返回错误值io.EOF。下面显示了类型alphaReader,这是io.Reader的一个简单实现,可以从字符串源中过滤出非字母字符:

type alphaReader string 

func (a alphaReader) Read(p []byte) (int, error) { 
   count := 0 
   for i := 0; i < len(a); i++ { 
         if (a[i] >= 'A' && a[i] <= 'Z') || 
               (a[i] >= 'a' && a[i] <= 'z') { 
               p[i] = a[i] 
         } 
         count++ 
   } 
   return count, io.EOF 
} 

func main() { 
   str := alphaReader("Hello! Where is the sun?") 
   io.Copy(os.Stdout, &str) 
   fmt.Println() 
} 

golang.fyi/ch10/reader0.go

由于alphaReader类型的值实现了io.Reader接口,因此它们可以在任何需要读卡器的地方参与,如对io.Copy(os.Stdout, &str)的调用所示。这会将alphaReader变量发出的字节流复制到写入器接口os.Stdout(稍后介绍)。

链接读卡器

标准库可能已经有了一个可以重用的读卡器——因此,包装现有读卡器并将其流用作新实现的源是很常见的。以下代码段显示了alphaReader的更新版本。本次以io.Reader为源,如下代码所示:

type alphaReader struct { 
   src io.Reader 
} 

func NewAlphaReader(source io.Reader) *alphaReader { 
   return &alphaReader{source} 
} 

func (a *alphaReader) Read(p []byte) (int, error) { 
   if len(p) == 0 { 
         return 0, nil 
   } 
   count, err := a.src.Read(p) // p has now source data 
   if err != nil { 
         return count, err 
   } 
   for i := 0; i < len(p); i++ { 
         if (p[i] >= 'A' && p[i] <= 'Z') || 
               (p[i] >= 'a' && p[i] <= 'z') { 
               continue 
         } else { 
               p[i] = 0 
         } 
   } 
   return count, io.EOF 
} 

func main() { 
   str := strings.NewReader("Hello! Where is the sun?") 
   alpha := NewAlphaReader(str) 
   io.Copy(os.Stdout, alpha) 
   fmt.Println() 
} 

golang.fyi/ch10/reader1.go

在这个版本的代码中需要注意的主要变化是,alphaReader类型现在是一个嵌入io.Reader值的结构。调用alphaReader.Read()时,将包装好的读取器调用为a.src.Read(p),将源数据注入字节片p。然后该方法循环通过p并对数据应用过滤器。现在,要使用alphaReader,必须首先提供一个现有的读卡器,该读卡器由NewAlphaReader()构造函数提供。

这种方法的优点一开始可能并不明显。然而,通过使用io.Reader作为底层数据源,alphaReader类型能够从任何读卡器实现中读取数据。例如,下面的代码片段显示了alphaReader类型现在如何与os.File组合,以从文件中过滤出非字母字符(Go 源代码本身):

... 
func main() { 
   file, _ := os.Open("./reader2.go") 
   alpha := NewAlphaReader(file) 
   io.Copy(os.Stdout, alpha) 
   fmt.Println() 
} 

golang.fyi/ch10/reader2.go

io.Writer 接口

io.Writer接口,如以下代码所示,与对应的读卡器一样简单:

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

该接口需要实现单个方法Write(p []byte)(c int, e error),该方法从提供的流p复制数据,并将该数据写入接收器资源,例如内存结构、标准输出、文件、网络连接或任意数量的io.WriterGo 标准库附带的实现。Write方法返回从p复制的字节数,如果遇到任何字节,则返回error值。

下面的代码片段显示了channelWriter类型的实现,该类型是一个编写器,它将通过 Go 通道发送的流分解并序列化为连续字节:

type channelWriter struct { 
   Channel chan byte 
} 

func NewChannelWriter() *channelWriter { 
   return &channelWriter{ 
         Channel: make(chan byte, 1024), 
   } 
} 

func (c *channelWriter) Write(p []byte) (int, error) { 
   if len(p) == 0 { 
         return 0, nil 
   } 

   go func() { 
         defer close(c.Channel) // when done 
         for _, b := range p { 
               c.Channel <- b 
         } 
   }() 

   return len(p), nil 
} 

golang.fyi/ch10/writer1.go

Write方法使用 goroutine 从p复制每个字节,并通过c.Channel发送。完成后,goroutine 关闭频道,以便通知消费者何时停止从频道消费。作为一种实现约定,编写者不应该修改切片p或保留它。发生错误时,写入程序应返回当前处理的字节数和错误。

使用channelWriter类型很简单。您可以直接调用Write()方法,或者更常见的是,将channelWriter与 API 中的其他 IO 原语一起使用。例如,以下代码段使用fmt.Fprint函数通过channelWriter"Stream me!"字符串序列化为通道上的字节序列:

func main() { 
   cw := NewChannelWriter() 
   go func() { 
         fmt.Fprint(cw, "Stream me!") 
   }() 

   for c := range cw.Channel { 
         fmt.Printf("%c\n", c) 
   } 
} 

golang.fyi/ch10/writer1.go

在前面的代码段中,在通道中排队的序列化字节在连续打印时使用for…range语句进行消耗。下面的代码片段显示了另一个示例,其中使用相同的[T1]通过通道序列化文件内容。在本实现中,使用io.File值和io.Copy函数来源数据,而不是使用fmt.Fprint函数:

func main() { 
   cw := NewChannelWriter() 
   file, err := os.Open("./writer2.go") 
   if err != nil { 
         fmt.Println("Error reading file:", err) 
         os.Exit(1) 
   } 
   _, err = io.Copy(cw, file) 
   if err != nil { 
         fmt.Println("Error copying:", err) 
         os.Exit(1) 
   } 

   // consume channel 
   for c := range cw.Channel { 
         fmt.Printf("%c\n", c) 
   } 
} 

golang.fyi/ch10/writer2.go。

使用 io 包

显然,从 IO 开始的地方是io包(https://golang.org/pkg/io )。正如我们已经看到的,io包将输入和输出原语定义为io.Readerio.Writer接口。下表总结了io软件包中提供的其他功能和类型,这些功能和类型有助于流式 IO 操作。

| **功能** | **说明** | | `io.Copy()` | `io.Copy`函数(及其变体`io.CopyBuffer`和`io.CopyN`可以轻松地将任意`io.Reader`源中的数据复制到同样任意的`io.Writer`接收器中,如以下代码片段所示:
data := strings.NewReader("Write   me down.")   
file, _ := os.Create("./iocopy.data")   
io.Copy(file, data)   

golang.fyi/ch10/iocopy。 | | PipeReader PipeWriter | io包包括PipeReaderPipeWriter类型,它们将 IO 操作建模为内存管道。数据写入管道的io.Writer并可在管道的io.Reader处独立读取。下面的简短片段演示了一个简单的管道,它将字符串写入写入器pw。然后使用pr读取器读取数据,并将其复制到一个文件中:

file, _ := os.Create("./iopipe.data")   
pr, pw := io.Pipe()    
go func() {   
    fmt.Fprint(pw, "Pipe   streaming")   
    pw.Close()   
}()   

wait := make(chan struct{})   
go func() {   
    io.Copy(file, pr)   
    pr.Close()   
    close(wait)   
}()   
<-wait //wait for pr to finish   

golang.fyi/ch10/iopipe.go 请注意,管道编写器将阻塞,直到读取器完全使用管道内容或遇到错误。因此,读者和作者都应该包装在 goroutine 中,以避免死锁。 | | io.TeeReader() | 与io.Copy功能类似,io.TeeReader将内容从读卡器传输到写卡器。但是,该函数还通过返回的[T2]发送复制的字节(未更改)。TeeReader 非常适合组合多步骤 IO 流处理。下面的缩写代码段首先使用[T0]计算文件内容的 SHA-1 哈希。生成的读取器data随后流式传输到 gzip 写入器zip

fin, _ := os.Open("./ioteerdr.go")   
defer fin.Close()   
fout, _ := os.Create("./teereader.gz")   
defer fout.Close()   

zip := gzip.NewWriter(fout)   
defer zip.Close()   
sha := sha1.New()   
data := io.TeeReader(fin, sha)    
io.Copy(zip, data)   

fmt.Printf("SHA1 hash %x\n",   sha.Sum(nil))   

golang.fyi/ch10/ioteerdr0.go 如果我们想同时计算 SHA-1 和 MD5,我们可以更新代码来嵌套两个TeeReader值,如下面的代码片段所示:

sha := sha1.New()   
md := md5.New()   
data := io.TeeReader(
  io.TeeReader(fin, md), sha,   
)    
io.Copy(zip, data)   

golang.fyi/ch10/ioteerdr1.go | | io.WriteString() | io.WriteString函数将字符串的内容写入指定的写入器。以下命令将字符串的内容写入文件:

fout, err := os.Create("./iowritestr.data")   
if err != nil {   
    fmt.Println(err)   
    os.Exit(1)   
}   
defer fout.Close()   
io.WriteString(fout, "Hello   there!\n")   

golang.fyi/ch10/iowritest.go | | io.LimitedReader | 顾名思义,io.LimitedReader结构是一个读卡器,它只从指定的io.Reader中读取N个字节。以下代码段将打印字符串的前 19 个字节:

str := strings.NewReader("The   quick brown " +       
    "fox jumps over the lazy   dog")   
limited :=   &io.LimitedReader{R: str, N: 19}   
io.Copy(os.Stdout, limited)   

golang.fyi/ch10/iolimitedr.go

$> go run iolimitedrd.go   
The quick brown fox   

| | io.SectionReader | io.SectionReader类型通过指定开始读取的索引(从零开始)和指示要读取的字节数的偏移量值来实现查找和跳过原语,如以下代码段所示:

str := strings.NewReader("The   quick brown"+   
    "fox jumps over the lazy   dog")   
section := io.NewSectionReader(str,   19, 23)   
io.Copy(os.Stdout, section)   

golang.fyi/ch10/iosectionr.go 本例将打印jumps over the lazy dog。 | | 包装io/ioutil | io/ioutil 子包实现了为 IO 原语提供实用快捷方式的少量功能,如文件读取、目录列表、临时目录创建和文件写入。 |

处理文件

os包(https://golang.org/pkg/os/ 公开表示系统上文件句柄的os.File类型。os.File类型实现了几个 IO 原语,包括io.Readerio.Writer接口,允许使用标准流式 IO API 处理文件内容。

创建和打开文件

os.Create函数使用指定路径创建新文件。如果文件已经存在,os.Create将覆盖它。另一方面,os.Open函数打开一个现有文件进行读取。

下面的源代码片段打开一个现有文件,并使用io.Copy函数创建其内容的副本。需要注意的一个常见且推荐的做法是延迟调用文件上的方法Close。这确保了在函数退出时操作系统资源的正常释放:

func main() { 
   f1, err := os.Open("./file0.go") 
   if err != nil { 
         fmt.Println("Unable to open file:", err) 
         os.Exit(1) 
   } 
   defer f1.Close() 

   f2, err := os.Create("./file0.bkp") 
   if err != nil { 
         fmt.Println("Unable to create file:", err) 
         os.Exit(1) 
   } 
   defer f2.Close() 

   n, err := io.Copy(f2, f1) 
   if err != nil { 
         fmt.Println("Failed to copy:", err) 
         os.Exit(1) 
   } 

   fmt.Printf("Copied %d bytes from %s to %s\n",  
       n, f1.Name(), f2.Name()) 
} 

golang.fyi/ch10/file0.go

函数 os.OpenFile

os.OpenFile函数提供通用的低级功能,用于创建新文件或打开现有文件,并对文件的行为及其权限进行细粒度控制。然而,通常使用os.Openos.Create函数,因为它们提供了比os.OpenFile函数更简单的抽象。

os.OpenFile功能采用三个参数。第一个参数是文件的路径,第二个参数是一个屏蔽位字段值,用于指示操作的行为(例如,只读、读写、截断等),最后一个参数是文件的 posix 兼容权限值。

下面的缩写源代码段重新实现了前面的文件复制代码。然而,这一次,它使用os.FileOpen函数来演示它是如何工作的:

func main() { 
   f1, err := os.OpenFile("./file0.go", os.O_RDONLY, 0666) 
   if err != nil {...} 
   defer f1.Close() 

   f2, err := os.OpenFile("./file0.bkp", os.O_WRONLY, 0666) 
   if err != nil {...} 
   defer f2.Close() 

   n, err := io.Copy(f2, f1) 
   if err != nil {...} 

   fmt.Printf("Copied %d bytes from %s to %s\n",  
      n, f1.Name(), f2.Name()) 
} 

golang.fyi/ch10/file1.go

如果您已经有了对 OS 文件描述符的引用,您还可以使用os.NewFile函数在程序中创建文件句柄。很少使用os.NewFile函数,因为通常使用前面讨论的文件函数初始化文件。

文件读写

我们已经了解了如何使用os.Copy函数将数据移入或移出文件。但是,有时需要完全控制写入或读取文件数据的逻辑。例如,以下代码段使用os.File变量fout,中的WriteString方法创建文本文件:

func main() { 
   rows := []string{ 
         "The quick brown fox", 
         "jumps over the lazy dog", 
   } 

   fout, err := os.Create("./filewrite.data") 
   if err != nil { 
         fmt.Println(err) 
         os.Exit(1) 
   } 
   defer fout.Close() 

   for _, row := range rows { 
         fout.WriteString(row) 
   } 
} 

golang.fyi/ch10/filewrite0.go

但是,如果数据源不是文本,则可以直接将原始字节写入文件,如以下源代码段所示:

func main() { 
   data := [][]byte{ 
         []byte("The quick brown fox\n"), 
         []byte("jumps over the lazy dog\n"), 
   } 
   fout, err := os.Create("./filewrite.data") 
   if err != nil { ... } 
   defer fout.Close() 

   for _, out := range data { 
         fout.Write(out) 
   } 
} 

golang.fyi/ch10/filewrite0.go

作为一种io.Reader,可以使用读取方法直接读取io.File类型的数据。这样就可以将文件内容作为原始字节片流进行访问。以下代码段将文件../ch0r/dict.txt的内容读取为原始字节,分配给切片p一次最多 1024 个字节块:

func main() { 
   fin, err := os.Open("../ch05/dict.txt") 
   if err != nil { 
         fmt.Println(err) 
         os.Exit(1) 
   } 
   defer fin.Close() 
   p := make([]byte, 1024) 
   for { 
         n, err := fin.Read(p) 
         if err == io.EOF { 
               break 
         } 
         fmt.Print(string(p[:n])) 
   } 
} 

golang.fyi/ch10/fileread.go

标准输入、输出和错误

os包包含三个预先声明的变量os.Stdinos.Stdoutos.Stderr,分别表示操作系统标准输入、输出和错误的文件句柄。以下代码段读取文件f1并使用os.Copy 功能将其内容写入io.Stdout标准输出(标准输入将在后面介绍):

func main() { 
   f1, err := os.Open("./file0.go") 
   if err != nil { 
         fmt.Println("Unable to open file:", err) 
         os.Exit(1) 
   } 
   defer f1.Close() 

   n, err := io.Copy(os.Stdout, f1) 
   if err != nil { 
         fmt.Println("Failed to copy:", err) 
         os.Exit(1) 
   } 

   fmt.Printf("Copied %d bytes from %s \n", n, f1.Name()) 
} 

golang.fyi/ch10/osstd.go

带 fmt 的格式化 IO

IO 最广泛使用的软件包之一是fmthttps://golang.org/pkg/fmt )。它附带了一系列用于格式化输入和输出的函数。fmt包最常见的用途是写入标准输出和读取标准输入。本节还重点介绍了使fmt成为 IO 最佳工具的其他功能。

打印到 io.Writer 接口

fmt包提供了几个功能,用于将文本数据写入 io.Writer 的任意实现。fmt.Fprintfmt.Fprintln函数使用默认格式写入文本,fmt.Fprintf支持格式说明符。以下代码段使用fmt.Fprintf 函数将metalloid数据的列格式列表写入指定的文本文件:

type metalloid struct { 
   name   string 
   number int32 
   weight float64 
} 

func main() { 
   var metalloids = []metalloid{ 
         {"Boron", 5, 10.81}, 
         ... 
         {"Polonium", 84, 209.0}, 
   } 
   file, _ := os.Create("./metalloids.txt") 
   defer file.Close() 

   for _, m := range metalloids { 
         fmt.Fprintf( 
               file, 
               "%-10s %-10d %-10.3f\n", 
               m.name, m.number, m.weight, 
         ) 
   } 
} 

golang.fyi/ch10/fmtfprint0.go

在前面的示例中,fmt.Fprintf函数使用格式说明符将格式化文本写入 io.Filefile变量。fmt.Fprintf函数支持大量格式说明符,其正确处理超出了本文的范围。有关这些规范的完整内容,请参阅在线文档。

打印到标准输出

fmt.Printfmt.Printffmt.Println与前面的Fprint系列功能完全相同。但是,它们将文本写入标准输出文件句柄os.Stdout(参见前面介绍的标准输出、输入和错误一节),而不是任意的io.Writer

下面的简短代码片段显示了上一个示例的更新版本,该示例将类金属列表写入标准输出,而不是常规文件。请注意,除了使用fmt.Printf而不是fmt.Fprintf功能外,代码相同:

type metalloid struct { ... } 
func main() { 
   var metalloids = []metalloid{ 
         {"Boron", 5, 10.81}, 
         ... 
         {"Polonium", 84, 209.0}, 
   } 

   for _, m := range metalloids { 
         fmt.Printf( 
               "%-10s %-10d %-10.3f\n", 
               m.name, m.number, m.weight, 
         ) 
   } 
} 

golang.fyi/ch10/fmtprint0.go

从 io.Reader 读取

fmt包还支持从io.Reader接口格式化读取文本数据。fmt.Fscanfmt.Fscanln函数可用于将多个由空格分隔的值读入指定参数。fmt.Fscanf函数支持格式说明符,以便对io.Reader实现中输入的数据进行更丰富、更灵活的解析。

以下缩写代码段使用函数fmt.Fscanf对包含行星数据的空格分隔文件(planets.txt)进行格式化输入:

func main() { 
   var name, hasRing string 
   var diam, moons int 

   // read data 
   data, err := os.Open("./planets.txt") 
   if err != nil { 
         fmt.Println("Unable to open planet data:", err) 
         return 
   } 
   defer data.Close() 

   for { 
         _, err := fmt.Fscanf( 
               data, 
               "%s %d %d %s\n", 
               &name, &diam, &moons, &hasRing, 
         ) 
         if err != nil { 
               if err == io.EOF { 
                     break 
               } else { 
                     fmt.Println("Scan error:", err) 
                     return 
               } 
         } 
         fmt.Printf( 
               "%-10s %-10d %-6d %-6s\n", 
               name, diam, moons, hasRing, 
         ) 
   } 

golang.fyi/ch10/fmtfscan0.go

代码从io.File变量data读取,直到遇到指示文件结束的io.EOF错误。它读取的每一行文本都使用格式说明符"%s %d %d %s\n"进行解析,格式说明符与文件中存储的记录的空格分隔布局相匹配。然后将每个解析的令牌分配给其各自的变量namediammoonshasRing,,这些变量使用fm.Printf 函数打印到标准输出。

从标准输入读取

[T1]、[T2]和[T3]用于从标准输入文件句柄[T4]读取数据,而不是从任意的[T0]读取数据。以下代码段显示了一个从控制台读取文本输入的简单程序:

func main() { 
   var choice int 
   fmt.Println("A square is what?") 
   fmt.Print("Enter 1=quadrilateral 2=rectagonal:") 

   n, err := fmt.Scanf("%d", &choice) 
   if n != 1 || err != nil { 
         fmt.Println("Follow directions!") 
         return 
   } 
   if choice == 1 { 
         fmt.Println("You are correct!") 
   } else { 
         fmt.Println("Wrong, Google it.") 
   } 
} 

golang.fyi/ch10/fmtscan1.go

在前面的程序中,fmt.Scanf函数使用格式说明符"%d"解析输入,从标准输入中读取整数值。如果读取的值与指定格式不完全匹配,则函数将抛出错误。例如,下面显示读取字符D而不是整数时发生的情况:

$> go run fmtscan1.go
A square is what?
Enter 1=quadrilateral 2=rectagonal: D
Follow directions!

缓冲 IO

到目前为止,涉及的大多数 IO 操作都是无缓冲的。这意味着每个读写操作都可能受到底层操作系统处理 IO 请求的延迟的负面影响。另一方面,缓冲操作通过在 IO 操作期间缓冲内存中的数据来减少延迟。bufio包(https://golang.org/pkg/bufio /)提供缓冲读写 IO 操作功能。

缓冲写作者和读者

bufio包提供了几个功能,可以使用io.Writer接口对 IO 流进行缓冲写入。以下代码段创建一个文本文件,并使用缓冲 IO 写入其中:

func main() { 
   rows := []string{ 
         "The quick brown fox", 
         "jumps over the lazy dog", 
   } 

   fout, err := os.Create("./filewrite.data") 
   writer := bufio.NewWriter(fout) 
   if err != nil { 
         fmt.Println(err) 
         os.Exit(1) 
   } 
   defer fout.Close() 

   for _, row := range rows { 
         writer.WriteString(row) 
   } 
   writer.Flush() 
} 

golang.fyi/ch10/bufwrite0.go

通常,bufio包中的构造函数通过包装现有io.Writer作为其底层源来创建缓冲写入程序。例如,前面的代码通过包装 io.File 变量fout使用bufio.NewWriter函数创建缓冲写入程序。

要影响内部缓冲区的大小,请使用构造函数bufio.NewWriterSize(w io.Writer, n int)指定内部缓冲区的大小。bufio.Writer类型还提供了写入原始字节的WriteWriteByte方法,以及写入 Unicode 编码字符的WriteRune方法。

读取缓冲流只需调用构造函数bufio.NewReader包装现有的io.Reader即可。下面的代码片段通过将file变量包装为其底层源来创建bufio.Reader变量reader

func main() { 
   file, err := os.Open("./bufread0.go") 
   if err != nil { 
         fmt.Println("Unable to open file:", err) 
         return 
   } 
   defer file.Close() 

   reader := bufio.NewReader(file) 
   for { 
         line, err := reader.ReadString('\n') 
         if err != nil { 
               if err == io.EOF { 
                     break 
               } else { 
                     fmt.Println("Error reading:, err") 
                     return 
               } 
         } 
         fmt.Print(line) 
   } 
} 

戈朗 T0

前面的代码使用reader.ReadString方法读取文本文件,使用'\n'字符作为内容分隔符。要影响内部缓冲区的大小,请使用构造函数bufio.NewReaderSize(w io.Reader, n int)指定内部缓冲区的大小。bufio.Reader类型还提供了从流中读取原始字节的读取读取字节读取字节方法,以及读取 Unicode 编码字符的读取符文方法。

扫描缓冲区

bufio包还提供用于扫描和标记io.Reader源缓冲输入数据的原语。bufio.Scanner类型使用分割方法扫描输入数据,以定义标记化策略。下面的代码片段显示了行星示例的重新实现(来自前面)。这一次,代码使用bufio.Scanner(而不是fmt.Fscan功能)使用bufio.ScanLines功能扫描文本文件的内容:

func main() { 
   file, err := os.Open("./planets.txt") 
   if err != nil { 
         fmt.Println("Unable to open file:", err) 
         return 
   } 
   defer file.Close() 

   fmt.Printf( 
         "%-10s %-10s %-6s %-6s\n", 
         "Planet", "Diameter", "Moons", "Ring?", 
   ) 
   scanner := bufio.NewScanner(file) 
   scanner.Split(bufio.ScanLines) 
   for scanner.Scan() { 
         fields := strings.Split(scanner.Text(), " ") 
         fmt.Printf( 
               "%-10s %-10s %-6s %-6s\n", 
               fields[0], fields[1], fields[2], fields[3], 
         ) 
   } 
} 

golang.fyi/ch10/bufscan0.go

使用bufio.Scanner分为四个步骤,如前一示例所示:

  • 首先,使用bufio.NewScanner(io.Reader) 创建扫描仪
  • 调用scanner.Split方法来配置如何标记内容
  • 使用scanner.Scan 方法遍历生成的令牌
  • 使用scanner.Text 方法读取标记化数据

代码使用预定义函数bufio.ScanLines使用行分隔符解析缓冲内容。bufio包附带了几个预定义的拆分器功能,包括扫描字节将每个字节扫描为令牌;扫描符文将 UTF-8 编码令牌扫描;以及扫描字将每个空格分隔的字扫描为令牌。

内存 IO

bytes包提供了通用原语,用于在存储在内存中的字节块上实现流式 IO,以bytes.Buffer类型表示。由于bytes.Buffer类型同时实现io.Readerio.Writer接口,因此使用流式 IO 原语将数据流式传输到内存或从内存中流出是一个很好的选择。

以下代码段在byte.Buffer变量book中存储了几个字符串值。然后将缓冲器流至os.Stdout

func main() { 
   var books bytes.Buffer 
   books.WriteString("The Great Gatsby") 
   books.WriteString("1984") 
   books.WriteString("A Tale of Two Cities") 
   books.WriteString("Les Miserables") 
   books.WriteString("The Call of the Wild") 

   books.WriteTo(os.Stdout) 
} 

golang.fyi/ch10/bytesbuf0。

可以很容易地更新相同的示例,以将内容流式传输到常规文件,如以下简短代码段所示:

func main() { 
   var books bytes.Buffer 
   books.WriteString("The Great Gatsby\n") 
   books.WriteString("1984\n") 
   books.WriteString("A Take of Two Cities\n") 
   books.WriteString("Les Miserables\n") 
   books.WriteString("The Call of the Wild\n") 

   file, err := os.Create("./books.txt") 
   if err != nil { 
         fmt.Println("Unable to create file:", err) 
         return 
   } 
   defer file.Close() 
   books.WriteTo(file) 
} 

golang.fyi/ch10/bytesbuf1。

数据编解码

Go 中 IO 的另一个常见方面是在数据流传输时将数据从一种表示形式编码到另一种表示形式。标准库的编码器和解码器,在编码包(中找到 https://golang.org/pkg/encoding/ ),使用io.Readerio.Writer接口在编码和解码期间利用 IO 原语作为流式传输数据的方式。

Go 支持多种编码格式,用于各种目的,包括数据转换、数据压缩和数据加密。本章将重点介绍使用GobJSON格式进行数据转换的编码和解码数据。在第 11 章编写网络程序中,我们将探索使用编码器转换数据,以便使用远程过程调用RPC进行客户端和服务器通信。

带 gob 的二进制编码

gob包(https://golang.org/pkg/encoding/gob 提供了一种编码格式,可用于将复杂的 Go 数据类型转换为二进制。Gob 是自描述的,这意味着每个编码的数据项都有一个类型描述。编码过程包括将 gob 编码的数据流式传输到 io.Writer,以便将其写入资源以供将来使用。

下面的代码片段显示了一个示例代码,该代码将变量books(具有嵌套值的Book类型片段)编码为gob格式。编码器将其生成的二进制数据写入 os.Writer 实例,在这种情况下,*os.File类型的file 变量:

type Name struct { 
   First, Last string 
} 

type Book struct { 
   Title       string 
   PageCount   int 
   ISBN        string 
   Authors     []Name 
   Publisher   string 
   PublishDate time.Time 
} 

func main() { 
   books := []Book{ 
         Book{ 
               Title:       "Leaning Go", 
               PageCount:   375, 
               ISBN:        "9781784395438", 
               Authors:     []Name{{"Vladimir", "Vivien"}}, 
               Publisher:   "Packt", 
               PublishDate: time.Date( 
                     2016, time.July, 
                     0, 0, 0, 0, 0, time.UTC, 
               ), 
         }, 
         Book{ 
               Title:       "The Go Programming Language", 
               PageCount:   380, 
               ISBN:        "9780134190440", 
               Authors:     []Name{ 
                     {"Alan", "Donavan"}, 
                     {"Brian", "Kernighan"}, 
               }, 
               Publisher:   "Addison-Wesley", 
               PublishDate: time.Date( 
                     2015, time.October, 
                     26, 0, 0, 0, 0, time.UTC, 
               ), 
         }, 
         ... 
   } 

   // serialize data structure to file 
   file, err := os.Create("book.dat") 
   if err != nil { 
         fmt.Println(err) 
         return 
   } 
   enc := gob.NewEncoder(file) 
   if err := enc.Encode(books); err != nil { 
         fmt.Println(err) 
   } 
} 

golang.fyi/ch10/gob0 go。

尽管前面的示例很长,但它主要是由分配给变量books的嵌套数据结构的定义构成的。最后六行或更多行是进行编码的地方。使用enc := gob.NewEncoder(file)创建 gob 编码器。只需调用enc.Encode(books)即可对数据进行编码,将编码后的数据流式传输到提供文件。

解码过程通过使用io.Reader流式传输 gob 编码的二进制数据并自动将其重建为强类型 Go 值来实现相反的目的。下面的代码片段对上一示例中编码并存储在books.data文件中的 gob 数据进行解码。解码器从io.Reader读取数据,在本例中为*os.File类型的变量file

type Name struct { 
   First, Last string 
} 

type Book struct { 
   Title       string 
   PageCount   int 
   ISBN        string 
   Authors     []Name 
   Publisher   string 
   PublishDate time.Time 
} 

func main() { 
   file, err := os.Open("book.dat") 
   if err != nil { 
         fmt.Println(err) 
         return 
   } 

   var books []Book 
   dec := gob.NewDecoder(file) 
   if err := dec.Decode(&books); err != nil { 
         fmt.Println(err) 
         return 
   } 
} 

golang.fyi/10/gob1。

通过使用dec := gob.NewDecoder(file)创建解码器来解码先前编码的 gob 数据。下一步是声明将存储解码数据的变量。在我们的示例中,[]Book类型的books变量被声明为解码数据的目的地。实际解码通过调用dec.Decode(&books)完成。请注意,Decode()方法将其目标变量的地址作为参数。解码后,books 变量将包含从文件流式传输的重构数据结构。

在撰写本文时,gob 编码器和解码器 API 仅在 Go 编程语言中可用。这意味着编码为 gob 的数据只能由 Go 程序使用。

将数据编码为 JSON

编码包还附带了一个json编码器子包(https://golang.org/pkg/encoding/json/ )支持 JSON 格式的数据。这大大拓宽了 Go 程序可以用来交换复杂数据结构的语言的数量。JSON 编码的工作原理与 gob 包中的编码器和解码器类似。不同之处在于,生成的数据采用明文 JSON 编码格式,而不是二进制格式。以下代码更新了前面的示例,将数据编码为 JSON:

type Name struct { 
   First, Last string 
} 

type Book struct { 
   Title       string 
   PageCount   int 
   ISBN        string 
   Authors     []Name 
   Publisher   string 
   PublishDate time.Time 
} 

func main() { 
   books := []Book{ 
         Book{ 
               Title:       "Leaning Go", 
               PageCount:   375, 
               ISBN:        "9781784395438", 
               Authors:     []Name{{"Vladimir", "Vivien"}}, 
               Publisher:   "Packt", 
               PublishDate: time.Date( 
                     2016, time.July, 
                     0, 0, 0, 0, 0, time.UTC), 
         }, 
         ... 
   } 

   file, err := os.Create("book.dat") 
   if err != nil { 
         fmt.Println(err) 
         return 
   } 
   enc := json.NewEncoder(file) 
   if err := enc.Encode(books); err != nil { 
         fmt.Println(err) 
   } 
} 

golang.fyi/ch10/json0.go

代码与以前完全相同。它使用分配给books变量的相同嵌套结构片。唯一的区别是编码器是用enc := json.NewEncoder(file)创建的,它创建了一个 JSON 编码器,将file变量用作其io.Writer目的地。当执行enc.Encode(books)时,变量books的内容被序列化为 JSON 到本地文件books.dat,如下代码所示(为可读性而格式化):

[ 
 {
 "Title":"Leaning Go",
 "PageCount":375,
 "ISBN":"9781784395438",
 "Authors":[{"First":"Vladimir","Last":"Vivien"}],
 "Publisher":"Packt",
 "PublishDate":"2016-06-30T00:00:00Z"
 },
 {
 "Title":"The Go Programming Language",
 "PageCount":380,
 "ISBN":"9780134190440",
 "Authors":[
 {"First":"Alan","Last":"Donavan"},
                      {"First":"Brian","Last":"Kernighan"}
 ],
 "Publisher":"Addison-Wesley",
 "PublishDate":"2015-10-26T00:00:00Z"
 },
 ...
]

文件 books.dat(格式化)

默认情况下,生成的 JSON 编码内容使用结构字段的名称作为 JSON 对象键的名称。可以使用 struct 标记控制此行为(请参阅“使用 struct 标记控制 JSON 映射”[T1]一节)。

在 Go 中使用 JSON 编码的数据是使用 JSON 解码器完成的,该解码器从io.Reader流式传输其源。下面的代码片段对存储在文件book.dat中的上一个示例中生成的 JSON 编码数据进行解码。请注意,数据结构(以下代码中未显示)与之前相同:

func main() { 
   file, err := os.Open("book.dat") 
   if err != nil { 
         fmt.Println(err) 
         return 
   } 

   var books []Book 
   dec := json.NewDecoder(file) 
   if err := dec.Decode(&books); err != nil { 
         fmt.Println(err) 
         return 
   } 
} 

golang.fyi/ch10/json1.go

books.dat 文件中的数据存储为 JSON 对象数组。因此,代码必须声明一个能够存储嵌套结构值的索引集合的变量。在前面的示例中,[]Book类型的books变量被声明为解码数据的目的地。实际解码通过调用dec.Decode(&books)完成。请注意,Decode()方法将其目标变量的地址作为参数。解码后,books变量将包含从文件流式传输的重构数据结构。

使用 struct 标记控制 JSON 映射

默认情况下,结构字段的名称用作生成的 JSON 对象的键。这可以通过使用struct类型标记来控制,以指定在数据编码和解码期间如何映射 JSON 对象密钥名称。例如,下面的代码片段使用[T1]标记前缀声明 struct 字段,以指定如何对对象键进行编码和解码:

type Book struct { 
   Title       string      `json:"book_title"` 
   PageCount   int         `json:"pages,string"` 
   ISBN        string      `json:"-"` 
   Authors     []Name      `json:"auths,omniempty"` 
   Publisher   string      `json:",omniempty"` 
   PublishDate time.Time   `json:"pub_date"` 
} 

golang.fyi/ch10/json2.go

下表总结了标签及其含义:

| **标签** | **说明** | | `Title string `json:"book_title"`` | 将[T0]结构字段映射到 JSON 对象键[T1]。 | | `PageCount int `json:"pages,string"`` | 将[T0]结构字段映射到 JSON 对象键[T1],并将值作为字符串而不是数字输出。 | | `ISBN string `json:"-"`` | 破折号导致编码和解码过程中跳过[T0]字段。 | | `Authors []Name `json:"auths,omniempty"`` | 将[T0]字段映射到 JSON 对象键[T1]。注释[T2]会导致字段在其值为 nil 时被忽略。 | | `Publisher string `json:",omniempty"`` | 将结构字段名`Publisher,`映射为 JSON 对象键名。注释`omniempty,`导致字段为空时被忽略。 | | `PublishDate time.Time `json:"pub_date"`` | 将字段名`PublishDate,`映射到 JSON 对象键`"pub_date"`。 |

当对上一个结构进行编码时,它会在books.dat文件中生成以下 JSON 输出(为可读性而格式化):

... 
{ 
   "book_title":"The Go Programming Language", 
   "pages":"380", 
   "auths":[ 
         {"First":"Alan","Last":"Donavan"}, 
         {"First":"Brian","Last":"Kernighan"} 
   ], 
   "Publisher":"Addison-Wesley", 
   "pub_date":"2015-10-26T00:00:00Z" 
} 
... 

请注意,JSON 对象键的标题与[T0]标记中指定的标题相同。对象键"pages"(映射到结构字段PageCount)被编码为字符串。最后,省略结构字段ISBN,,如struct标记中所注释。

自定义编码和解码

JSON 包使用两个接口,封送器解编器分别钩住编码和解码事件。当编码器遇到类型实现了json.Marshaler的值时,它将该值的序列化委托给 Marshaller 接口中定义的方法MarshalJSON。这在以下简短代码段中得到了示例,其中更新了类型Name以实现json.Marshaller,如图所示:

type Name struct { 
   First, Last string 
} 
func (n *Name) MarshalJSON() ([]byte, error) { 
   return []byte( 
         fmt.Sprintf(""%s, %s"", n.Last, n.First) 
   ), nil 
} 

type Book struct { 
   Title       string 
   PageCount   int 
   ISBN        string 
   Authors     []Name 
   Publisher   string 
   PublishDate time.Time 
} 
func main(){ 
   books := []Book{ 
         Book{ 
               Title:       "Leaning Go", 
               PageCount:   375, 
               ISBN:        "9781784395438", 
               Authors:     []Name{{"Vladimir", "Vivien"}}, 
               Publisher:   "Packt", 
               PublishDate: time.Date( 
                     2016, time.July, 
                     0, 0, 0, 0, 0, time.UTC), 
         }, 
         ... 
   } 
   ... 
   enc := json.NewEncoder(file) 
   if err := enc.Encode(books); err != nil { 
         fmt.Println(err) 
   } 
} 

golang.fyi/ch10/json3.go

在前面的示例中,Name类型的值被序列化为 JSON 字符串(而不是前面的对象)。序列化由方法Name.MarshallJSON处理,该方法返回一个字节数组,其中包含用逗号分隔的姓和名。前面的代码生成以下 JSON 输出:

 [
 ...
 {
                "Title":"Leaning Go",
                "PageCount":375,
                "ISBN":"9781784395438",
                "Authors":["Vivien, Vladimir"],
                "Publisher":"Packt",
                "PublishDate":"2016-06-30T00:00:00Z"
          },
          ...
    ] 

相反,当解码器遇到一段映射到实现[T0]的类型的 JSON 文本时,它将解码委托给该类型的[T1]方法。例如,下面显示了实现json.Unmarshaler以处理Name类型的 JSON 输出的缩写代码段:

type Name struct { 
   First, Last string 
} 

func (n *Name) UnmarshalJSON(data []byte) error { 
   var name string 
   err := json.Unmarshal(data, &name) 
   if err != nil { 
         fmt.Println(err) 
         return err 
   } 
   parts := strings.Split(name, ", ") 
   n.Last, n.First = parts[0], parts[1] 
   return nil 
} 

golang.fyi/ch10/json4.go

Name类型是json.Unmarshaler的一个实现。当解码器遇到键为"Authors"的 JSON 对象时,它使用方法Name.Unmarshaler从 JSON 字符串中重新构造 Go 结构Name 类型。

Go 标准库提供了额外的编码器(此处未涉及),包括base32bas364binarycsvhexxmlgzip和众多加密格式编码器。

总结

本章提供了 Go 的数据输入和输出习惯用法以及实现 IO 原语所涉及的包的高级视图。本章首先介绍了使用[T0]和[T1]接口的基于流的 IO 的基本原理。读者将仔细阅读 anio.Reader和 anio.Writer的实施策略和示例。

本章继续介绍支持流式 IO 机制的包、类型和功能,包括处理文件、格式化 IO、缓冲 IO 和内存 IO。本章的最后一部分介绍在数据流传输时转换数据的编码器和解码器。在下一章中,当讨论转向创建使用 IO 通过网络进行通信的程序时,将进一步讨论 IO 主题。