本书的前几章主要关注基础知识。在本章和以后的章节中,读者将了解 Go 的标准库提供的一些功能强大的 API。本章详细讨论了如何使用标准库及其各自包中的 API 输入、处理、转换和输出数据,主题如下:
- 与读者和作家的对话
- io.Reader 接口
- io.Writer 接口
- 使用 io 包
- 使用文件
- 用 fmt 格式化 IO
- 缓冲 IO
- 内存 IO
- 数据编解码
与 Java 等其他语言类似,Go 将数据输入和输出建模为从源流向目标的流。数据资源,例如文件、网络连接,甚至一些内存对象,可以建模为字节流,从中可以读取或写入,如下图所示:
数据流表示为可被访问以进行读取或写入的字节(【】字节)的片。正如我们将在本章中探讨的, *io*
包提供io.Reader
接口,以实现读取并将数据从源传输到字节流的代码。相反,io.Writer
接口允许实现者创建代码,从提供的字节流读取数据,将作为输出写入目标资源。这两个接口在 Go 中被广泛用作表示 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
接口,如以下代码所示,与对应的读卡器一样简单:
type Writer interface {
Write(p []byte) (n int, err error)
}
该接口需要实现单个方法Write(p []byte)(c int, e error)
,该方法从提供的流p
复制数据,并将该数据写入接收器资源,例如内存结构、标准输出、文件、网络连接或任意数量的io.Writer
Go 标准库附带的实现。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
包(https://golang.org/pkg/io )。正如我们已经看到的,io
包将输入和输出原语定义为io.Reader
和io.Writer
接口。下表总结了io
软件包中提供的其他功能和类型,这些功能和类型有助于流式 IO 操作。
data := strings.NewReader("Write me down.")
file, _ := os.Create("./iocopy.data")
io.Copy(file, data)
golang.fyi/ch10/iocopy。 |
| PipeReader PipeWriter
| io
包包括PipeReader和PipeWriter类型,它们将 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.Reader
和io.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.Open
和os.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.Stdin
、os.Stdout
和os.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
IO 最广泛使用的软件包之一是fmt
(https://golang.org/pkg/fmt )。它附带了一系列用于格式化输入和输出的函数。fmt
包最常见的用途是写入标准输出和读取标准输入。本节还重点介绍了使fmt
成为 IO 最佳工具的其他功能。
fmt
包提供了几个功能,用于将文本数据写入 io.Writer 的任意实现。fmt.Fprint
和fmt.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.Print
、fmt.Printf
和fmt.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
fmt
包还支持从io.Reader
接口格式化读取文本数据。fmt.Fscan
和fmt.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"
进行解析,格式说明符与文件中存储的记录的空格分隔布局相匹配。然后将每个解析的令牌分配给其各自的变量name
、diam
、moons
和hasRing,
,这些变量使用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 操作期间缓冲内存中的数据来减少延迟。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
类型还提供了写入原始字节的Write
和WriteByte
方法,以及写入 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 编码令牌扫描;以及扫描字将每个空格分隔的字扫描为令牌。
bytes
包提供了通用原语,用于在存储在内存中的字节块上实现流式 IO,以bytes.Buffer
类型表示。由于bytes.Buffer
类型同时实现io.Reader
和io.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.Reader
和io.Writer
接口在编码和解码期间利用 IO 原语作为流式传输数据的方式。
Go 支持多种编码格式,用于各种目的,包括数据转换、数据压缩和数据加密。本章将重点介绍使用Gob和JSON格式进行数据转换的编码和解码数据。在第 11 章编写网络程序中,我们将探索使用编码器转换数据,以便使用远程过程调用(RPC进行客户端和服务器通信。
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编码器子包(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
变量将包含从文件流式传输的重构数据结构。
默认情况下,结构字段的名称用作生成的 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 标准库提供了额外的编码器(此处未涉及),包括base32
、bas364
、binary
、csv
、hex
、xml
、gzip
和众多加密格式编码器。
本章提供了 Go 的数据输入和输出习惯用法以及实现 IO 原语所涉及的包的高级视图。本章首先介绍了使用[T0]和[T1]接口的基于流的 IO 的基本原理。读者将仔细阅读 anio.Reader
和 anio.Writer
的实施策略和示例。
本章继续介绍支持流式 IO 机制的包、类型和功能,包括处理文件、格式化 IO、缓冲 IO 和内存 IO。本章的最后一部分介绍在数据流传输时转换数据的编码器和解码器。在下一章中,当讨论转向创建使用 IO 通过网络进行通信的程序时,将进一步讨论 IO 主题。