通向Golang的捷径【12. 读取和写入】

除了 fmt 和 os 包之外, 还需要导入 bufio 包, 以实现带缓冲的输入和输出操作.

12.1 读取用户输入

从键盘或标准输入端 (即 os.Stdin), 可读取用户输入, 当然最简单的方式则是采用 fmt 包中, 给出 Scan-或Sscan-前缀的函数, 如下:

例 12.1 readinput1.go

在这里插入图片描述
Scanln 可从标准输入端中, 读取文本输入, 并将连续出现的文本 (以空格分隔), 放入到连续的形参中, 并在一个新行 (给出回车键) 出现时, 停止读取操作,Scanf 可实现与 Scanln 相同的功能, 但它的第一个形参为格式化字符串, 因此读取内容将放入到该字符串中, 而 Sscan 相关函数也可实现上述的类似功能, 但这类函数只能读取输入字符串 (而不是标准输入端), 同时还能检查读入元素的个数, 以及函数运行的错误. 在以下示例中, 将使用一个来自于 bufio 包的读取器 (包含缓冲).

例 12.2 readinput2.go

在这里插入图片描述
在这里插入图片描述
inputReader 将指向 bufio 包的读取器, 并会创建该读取器, 且链接到标准输入端:
在这里插入图片描述
其中包含了 io.Reader 接口的形参 (这类接口可给出 Read() 方法, 参见 11.8), 上述函数可基于特定设备 (比如 os.Stdin), 返回一个新的带缓冲的 io.Reader 读取器.

上述读取器还包含了一个方法 ReadString(delim byte), 它可实现输入端的读取, 直到界定符出现, 而 delim 即为界定符, 而读取数据将放入到一个缓冲中. ReadString 可返回读取的字符串以及错误码, 如果文件末尾出现时, 该方法可返回读取的字符串和 io.EOF, 如果 delim 未出现, 将返回一个非 nil 的错误码.

当捕获回车键 (’\n’), 将实现键盘读取, 标准输出端 os.Stdout 即为屏幕, 标准错误端 os.Stderr 可提供错误信息, 大多数情况下, 等同于 os.Stdout.

在 Go 代码中, 应尽量避免声明全局变量 (即使用 =), 最好使用:= 符号, 声明一个变量, 如下:
在这里插入图片描述
在以下示例中, 将使用不同版本的 switch 语句, 来测试键盘输入:

例 12.3 switch_input.go

在这里插入图片描述
在这里插入图片描述

12.2 文件的读取和写入

12.2.1 文件读取

Go 语言的文件是一个 os.File 类型的指针, 也被称为文件句柄, 在 *os.File 文件类型中, 还可使用标准输入端(os.Stdin) 和输出端 (os.Stdout), 如下:

例 12.4 fileinput.go

在这里插入图片描述
在这里插入图片描述
inputFile 变量的类型为 *os.File, 其实是一个结构类型, 用于表示一个文件描述符 (文件句柄), 在 os 包的 Open函数中, 可打开文件, Open 函数可接收一个文件名字符串, 上例是 input.dat, 并以只读方式打开该文件.

在inputFile, inputError = os.Open(”input.dat”) 语句中, 如果文件不存在, 或是应用程序未使用正确的方式打开文件,Open 函数都将产生一个错误, 如果文件被打开, 可使用defer.Close(), 以保证 Open 函数返回前, 可自动关闭已打开的文件, 之后可使用 bufio.NewReader, 得到一个读取器.

当使用 bufio 包的读取器 (写入器的功能也相同), 则意味着编程者将面对更方便的字符串对象, 并能够完全隔离于, 描述磁盘文件的底层数据流字节. 之后将在一个 for 循环中, 使用 ReadString( ‘\n’) 或ReadBytes( ‘\n’),读取文件的每一文本行 (此时的行尾界定符为\n). 由于在 Unix 和 Windows 系统中, 行尾界定符并不相同, 所以使用 ReadLine() 方法, 则是一种更好的替代方式.

当读取到文件末尾, 即readerError != nil(io.EOF 为真), 将使用 return 语句, 退出 for 循环.

应用示例

  1. 将整个文件都读入到一个字符串中:

如果这是一个应用需求, 可使用 io/ioutil 包的 ioutil.ReadFile() 方法, 它可将读入的字节数据, 返回给
[]byte, 以及返回一个错误码, 同样 WriteFile() 方法也可将一个 []byte 数组, 写入到一个文件中.

例 12.5 read_write_file1.go

在这里插入图片描述
2. 带缓冲的读取操作:

除了 ReadString() 之外, 还可使用一种更通用的方式来读取文件, 也就是不将文件视为多个文本行, 或是
一个二进制文件. 这时可使用 bufio.Reader 类型的 Read() 方法, 如下:
在这里插入图片描述
3. 从文件中读取每列数据:

如果在文件中, 使用了空格, 来分隔不同的数据列, 可使用 fmt 包中, 以 FScan 为前缀的函数, 在以下示
例中, 可将三列数据, 读入 v1,v2,v3 变量中, 之后会将读出的变量, 附加到一个 slice 中, 以便还原对应的
数据列.

例 12.6 read_file2.go

在这里插入图片描述
path 包的 filepath 子包, 可提供文件名的跨平台处理函数, 比如 Base() 函数, 可返回路径的文件名, 且不包含路径分隔符 (/或).

12.2.2 压缩文件的读取

来自标准库的 compress 包, 可提供 bzip2,flate,gzip,lzw,zlib 等格式的压缩文件的读取. 以下示例将给出 gzip文件的读取.

例 12.7 gzipped.go

在这里插入图片描述
在这里插入图片描述

12.2.3 文件写入

例 12.8 fileoutput.go

在这里插入图片描述
在这里插入图片描述
除了文件句柄之外, 还需要一个 bufio 包的写入器, 以只写模式, 打开文件 output.dat, 如果该文件不存在, 将创建它:
在这里插入图片描述
在 OpenFile 函数中, 给出了一个文件名, 一个或多个标志位 (可使用逻辑或, 组合多个标志位), 以及文件权限,常用的标志位如下:
• os.O_RDONLY: 只读访问的标志位
• os.WRONLY: 只写访问的标志位
• os.O_CREATE: 当文件不存在时, 可用的创建标志位
• os.O_TRUNC: 当文件已存在, 可将文件截断为零尺寸的截断标志位

当文件读取时, 文件权限将被忽略, 并使用 0 值, 当文件写入时, 必须使用标准的 Unix 文件权限值 (0666), 在Windows 系统中, 也需要使用. 之后将创建写入器 (缓冲),
在这里插入图片描述
for 循环将重复执行, 对缓冲的写入 (outputWriter.WriteString(outputString)), 写入次数为 10 次, 之后的缓冲数据将完整写入文件中 (outputWriter.Flush()).

在简单的写入任务中, 可使用更高效的方式:
在这里插入图片描述
这里使用了包含 F 前缀的打印函数 (来自 fmt 包), 它可使用 io.Writer 写入器, 对文件进行写入, 参见 12.8 节. 以下示例中, 给出了 fmt.Fprintf 的另一种替代方式:

例 12.9 filewrite.go

在这里插入图片描述
os.Stdout.WriteString(”hello, world\n”) 可实现屏幕写入,
在这里插入图片描述
可创建并打开一个只写文件 test, 可能存在的错误码将被丢弃. 此处不会使用缓冲, 而是直接写入文件 (f.WriteString()).

12.3 文件复制

使用 io 包的 Copy 函数, 可实现最简单的文件复制:

例 12.10 filewrite.go

在这里插入图片描述
在这里插入图片描述
注意 defer 的用法, 当目的文件打开时, 出现了一个错误并返回, 这时将会执行 defer src.Close(), 如果省略该语句, 源文件将一直打开, 并会占用资源.

12.4 命令行参数的读取

12.4.1 os 包

os 包提供了一个 os.Args 变量, 它的类型为字符串 slice, 并能用于命令行参数的处理, 当应用程序启动时, 命令行参数将被读取到上述变量中, 如下:

例 12.11 os_args.go

在这里插入图片描述
在编辑器或 IDE 中, 运行以上程序, 输出结果为 Good Morning Alice, 如果在命令行中, 运行以上程序, 也可得到相同的输出结果, 如果在命令行中加入其他的参数, 比如os_args John Bill Marc Luke, 输出结果则为 GoodMorning Alice John Bill Marc Luke.

当应用程序启动时, 至少有一个命令行参数, 而命令行参数 (使用空格, 分隔不同参数) 将被放入 os.Args[] 变量, 同时索引值将从 1 开始 (os.Args[0] 将包含可执行程序的文件名, 此处为os_args), 在 strings.Join 函数中,又在命令行参数之间, 加入了一个空格.

12.4.2 flag 包

flag 包可对命令行参数进行解析, 通常用于替换一些常量, 比如在某些情况下, 需要为命令行的常量参数, 提供一个不同的数值, 在 flag 包中, 提供了一种结构类型 Flag:
在这里插入图片描述
在以下示例中, 模拟了 Unix 系统的 echo 工具:

例 12.12 echo.go

在这里插入图片描述
flag.Parse() 将获取命令行的参数列表 (或是常量参数列表), 并配置 flag, flag.Arg(i) 是指第 i 个参数, Parse()执行后, 所有 flag.Arg(i) 都有效, flag.Arg(0) 是指第一个有效参数, 而不是程序名 (即 os.Args(0)), flag.NArg() 将给出命令行参数的个数, 可在命令行参数解析 (Parse()) 后获得.

flag.Bool() 可定义一个 flag(标记), 它的默认值为 false, 当第一个参数 (此处是 n) 出现在命令行中, 该 flag 将为真 (NewLine 的类型为 *bool), 如果是 *NewLine 类型, 可实现 flag 的反向引用, 所以当加入新行后,flag 将为真.

flag.PrintDefaults() 可打印出预定义 flag 的用法信息, 比如
在这里插入图片描述
flag.VisitAll(fn func(*Flag)) 是另一个有价值的函数, 如果设定的 flag 按字母顺序排列, 将基于每个 flag 而调用 fn, 参见 15.8 节.

如果在命令行中 (Windows 系统), 给出命令echo.exe A B C, 程序将输出:A B C, 如果命令为echo.exe -n A B C, 程序将输出:
在这里插入图片描述
因此换行符已被打印, 所以在用法信息中, 将会包含-n=false: print newline. flag.Bool 可创建一个布尔型标记, 并能用于代码测试, 比如 processedFlag 标记的定义:
在这里插入图片描述
之后基于反向引用, 可在代码进行测试:
在这里插入图片描述
当然也可定义其他类型的标记, 比如 flag.Int(),flag.Float64,flag.String(), 可参见 15.8 节的示例.

12.5 文件的缓冲读取

在以下示例中, 将组合使用文件的缓冲读取和命令行标记的解析, 即使未输入命令行参数, 也会输出相应的信息. 如果给出文件名, 且文件存在, 将会显示文件内容, 测试命令为 cat test.

例 12.13 cat.go

在这里插入图片描述
在这里插入图片描述

12.6 文件读取和写入的 slice 存储

在处理 IO 缓冲时, 使用 slice 存储, 是一种标准的 Go 编程方式, 在以下的 cat 示例中, 将使用一个 for 循环,把文件读入一个 slice 缓冲中, 并会将读取内容, 写入到标准输出端.
在这里插入图片描述
在 cat2.go 示例中, 将使用 os.file 和 os 包的 Read 方法, 并会使用与上例相同的解决方案:

例 12.14 cat2.go

在这里插入图片描述
在这里插入图片描述

12.7 使用 defer 关闭文件

defer 关键字可保证打开文件的自动关闭 (在函数返回之前), 如下:
在这里插入图片描述

12.8 fmt.Fprintf 的用法

以下示例将给出 io 操作中的接口用法:

例 12.15 io_interfaces.go

在这里插入图片描述
在这里插入图片描述
fmt.Fprintf() 的原型如下:
在这里插入图片描述
fmt.Fprintf() 可将一个格式化字符串, 写入到第一个形参 (即必须实现的 io.Writer), Fprintf() 还可写入任意类型 (该类型实现了 Write 方法), 比如 os.Stdout, 文件 (如 os.File), 管道, 网络连接,channel 等, 也可写入来自 bufio 包的缓冲类型, 同时 bufio 包定义了 Writer 类型, 并实现了 Write 方法:
在这里插入图片描述
同时还提供了一个工厂函数, 即传入一个 io.Writer, 可返回一个带缓冲的 io.Writer(bufio.Writer 类型),
在这里插入图片描述
注意, 上述的写入操作都是缓冲操作, 因此在结束缓冲写入时, 不要忘记使用 Flush(), 否则最终的目标写入无法完成. 在 15.2-15.8 节中, 将使用 fmt.Fprint 编写一个 http.ResponseWriter, 其中也需要使用 io.Writer.

12.9 json 数据格式

通过网络传输或是文件存储进行数据传递时, 必须使用编码和解码操作, 目前已出现了大量的编码格式, 比如JSON,XML,gob,Google 缓冲协议, 以及其他格式, 而 Go 语言已支持了大多数的编码格式, 本章将讨论前三种编码格式.

结构可包含二进制数据, 如果将这些数据作为文本进行打印, 则无法被人理解, 同时这些数据也不会包含结构的数据域名, 因此也无法了解这些数据的意义.

为了改进某些编码格式, 将传输文本数据, 并会附加对应的数据域名, 以便这类数据的读取和理解, 同时这类数据也可进行网络传输, 因此这类数据也与平台无关, 并能在所有类别的应用中, 实现输入和输出, 而不必在意不同的编程语言或操作系统. 以下将给出通用的格式变换:
• 数据结构 → 特殊格式: 编码操作 (发送器或发送源在发送之前, 所需的操作)
• 特殊格式 → 数据结构: 解码操作 (接收器或接收源在接收之后, 所需的操作)

基于输出数据流 (io.Writer 输出), 也可实现编码操作, 同时解码数据也可作为输入数据流 (传递给 io.Reader),而编码和解码操作通常都会使用一个数据结构.

XML 格式的应用更广泛 (参见 12.10 节), 有时也会选择其他的主流格式, 其中最简单的格式为 JSON (JavaScript Object Notation, 主页为http://json.org), 通常会用于 web 后台和 JSP 程序 (在浏览器中运行) 之间的通讯, 当然也适应于其他应用. 以下是一个简短的 JSON 片段:
在这里插入图片描述
虽然 XML 的应用更广泛, 但 JSON 的描述更加简洁, 因此它对内存空间, 磁盘空间和网络带宽的需求更低, 同时 JSON 具有更好的可读性, 所以它的应用正在逐步增加.

json 包为了 Go 程序中 JSON 数据的读取和写入, 提供了一种简单的方式. 在以下示例中, 将使用 Address 和VCard 结构, 同时在本书的示例中, 为了代码的简洁, 都省略了错误的处理, 但在实际的程序中, 必须谨慎处理程序中出现的错误.

例 12.16 json.go

在这里插入图片描述
json.Marshal() 函数的原型为:
在这里插入图片描述
该函数可将输入数据, 编码成 json 文本, 并存入 []byte:
在这里插入图片描述
为了 web 应用的安全性, 最好使用 json.MarshalForHTML() 函数进行编码, 它可在数据中, 执行一次 HTMLEscape, 以使文本能够安全地嵌入到 HTML 的

并不是所有人都需要了解 json 编码, 只有描述对象的数据结构, 需要实现 JSON 编码,
• JSON 对象只需支持 string(将被视为 key), 同时用于编码的 Go map 类型, 必须是一个 map[string]T, 而T 为 json 包支持的任意 Go 类型.
• channel, 复数, 函数类型无法进行编码.
• 不支持循环数据结构, 因为它们需要在一个循环中进行编码.
• 指针的编码, 等同于所指数值的编码 (如果指向 nil, 数值则为 null).

json 只能访问结构类型的可导出数据域, 也只有这些数据域能被 JSON 处理, 因为 json 包将使用反射.

UnMarshal() 函数的原型如下:
在这里插入图片描述
它可从 json 格式数据中, 解码出数据结构.

首先将创建一个 Message 结构, 它可用于保存解码数据, 即 var m Message, 之后将调用 UnMarshal(), 并传入[]byte 类型的 JSON 编码数据 b, 以及 m 指针,
在这里插入图片描述
基于反射功能, 该函数会进行 json 域和结构数据域的匹配, 只有实现匹配, 才会进行数据的充填, 如果两种域之间未发生匹配, 也不会产生错误, 只是被简单丢弃.

在 json 包中, 将使用map[string]interface{} 和[]interface{}, 来保存任意的 JSON 对象和数组, 并能将有效的JSON 块, 解码到一个interface{} 中. 假定变量 b 中, 保存了以下的 JSON 数据:
在这里插入图片描述
其中未给出目的数据结构的描述, 因此需将其解码到一个interface{} 中:
在这里插入图片描述
此处的 f 应当是一个 map, 它的 key 为字符串 (这一点 JSON 数据可满足), value 则为 JSON 数据提供的数值, 并能视为一个空接口, 如下:
在这里插入图片描述
为了安全访问 f 的下层类型map[string]interface{} 中保存的数据, 则使用一个类型断言:
在这里插入图片描述
之后将基于 range 语句, 对 map 类型进行循环迭代, 并使用类型 switch 语句, 为不同的类型, 提供不同的处理:
在这里插入图片描述
在上述的处理方式中, 即使存在未知的 JSON 数据, 也可实现正常处理.

如果预先了解 json 数据的意义, 可定义一个相匹配的结构, 并将 json 数据解码到该结构中, 基于之前的示例, 可加入以下定义:
在这里插入图片描述
在上述处理中, 需要分配一个新的 slice, 而这也是相关引用类型 (指针, slice,map) 的典型解码操作.

在 json 包中, 还提供了 Decoder 和 Encoder 类型, 用于支持 JSON 数据的读取数据流和写入数据流的通用操作, 同时 NewDecoder 和 NewEncoder 函数封装了 io.Reader 和 io.Writer 接口类型.
在这里插入图片描述
为将 json 数据直接写入文件, 可使用文件类型实现的 json.NewEncoder(或使用其他类型实现的 io.Writer), 其中会将数据传入 Encode(), 相反的解码操作, 则可使用 json.Decoder 和 Decode() 函数:
在这里插入图片描述
基于之前的描述, 已了解泛型接口的实现方式, 数据结构可随意创建, 但必须实现 interface{}, 发送源和接收源必须实现 io.Writer 或 io.Reader, 基于大量的读取器和写入器, 能在大多数情况下, 使用 Encoder 和 Decoder类型, 比如 HTTP 连接,websocket 或文件的读取和写入.

12.10 xml 数据格式

一个 XML 的编码片段如下:
在这里插入图片描述
与 json 包类似,xml 包可提供了 Marshal() 和 UnMarshal() 函数, 用于 XML 数据的编码和解码, 但它可使用一种更通用的方式, 进行文件的读取和写入 (或是使用其他类型实现的 io.Reader 和 io.Writer).

与 json 相同,xml 数据也可实现与数据结构之间的编码和解码, 参见 15.8 节的示例. 在 encoding/xml 包中, 还实现了一个简单的 xml 解析器 (SAX), 它可读取 XML 数据并进行解析. 以下示例展示了该解析器的用法:

例 12.17 json.go

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
xml 包还定义了一组 XML 标记的类型, 比如 StartElement,Chardata(用于文本的起始和结束标记), EndElement, Comment,Directive 和 ProcInst.

同时还定义了一个 Parser 结构, 而 NewParser 方法可传入一个 io.Reader(有时是 strings.NewReader), 并能生成一个 Parser 类型的对象, 除此之外, 还提供了 Token() 方法, 它可返回输入数据流的下一个 XML token, 当处于输入数据流的末尾时,Token() 将返回 nil(即 io.EOF).

在一个 for 循环中, 可遍历 XML 文本, 当 Token() 方法返回一个错误时, 则表明 XML 文本已遍历完毕, 通过一个类型 switch 语句, 可基于不同的 XML 标记, 完成不同的处理, Chardata 标记的内容, 是一个 []byte 类型, 如果转换成字符串, 则更容易理解.

12.11 基于 gob 的数据传送

gob 是 Go 语言的一种自定义格式, 可实现二进制数据的串行和并行, 它也是 encoding 包的子包, 同时 gob 也就是 Go binary format 的简写, 与 Python 的 pickle 和 Java 的 Serialization 功能很相似.

该格式通常用于函数的传输参数, 以及远程调用 (RPCs,remote procedure calls, 可参考 15.9 节), 能在应用程序之间以及设备之间, 实现更通用的数据传输方式, 它与 json,xml 的不同点在于,gob 是为 Go 语言的系统环境所定制的, 比如使用 Go 语言实现的两服务器之间的通讯, 所以使用 gob 将得到更好的效率和优化, 同时它还无法成为一种和语言无关的编码格式, 这也就是它使用二进制数据, 而不是文本数据 (JSON 和 XML 使用了文本数据) 的原因, 所以 gob 无法在其他语言中使用, 因为它在编解码的处理中, 需要使用 Go 语言的反射功能.

gob 文件或数据流可实现自描述, 也就是每种类型中, 都将包含本类型的描述, 同时能在 Go 语言中实现解码, 并且无须了解文件的内容.

只有可导出的结构数据域, 才能被编码, 无须考虑零值, 在结构的解码中, 只有名称和类型能够匹配的数据域,才可实现解码, 在 gob 解码器客户端一直运行的情况下, 如果发送端中增加了新的数据域, 那么解码器客户端还将使用之前的数据域, 而不会处理新的数据域, 同时还可提供其他的灵活性, 比如整型可编码成可变长度的数据, 且不会在意发送端中 Go 类型的大小, 假如发送端给出以下结构 T:
在这里插入图片描述
接收端将收到 T 结构, 并将其送入结构变量 u(类型为 U).
在这里插入图片描述
其中 X 将获得数值 7,Y 将获得数值 0.

和 json 一样,gob 也需要使用 NewEncoder() 函数 (并会调用 Encode()), 以创建一个 Encoder 对象 (编码器), 再基于 io.Writer 实现通用处理操作, 在相反的解码处理中, 则需要使用 NewDecoder() 函数 (并会调用 Decode()), 以创建一个 Decoder 对象 (解码器), 再基于 io.Reader 实现通用处理操作.

以下将给出一个简单的编解码示例, 这里将使用字节缓冲, 来模拟网络传输.

例 12.18 gob1.go

在这里插入图片描述
在这里插入图片描述
下例可将编码数据直接写入文件:

例 12.19 gob2.go

在这里插入图片描述
在这里插入图片描述

12.12 Go 语言的密码术

基于网络的数据传输, 必须进行加密, 以使黑客无法读取或修改这些数据, 而数据发送时得到的校验值, 与数据接收后算出的校验值必须一致, 另外在 Go 语言的标准库中, 已有超过 30 个包, 可用于网络传输.
• hash 包: 实现了 adler32,crc32,crc64 和 fnv 等校验方式
• crypto 包: 实现了 md4,md5,sha1 等哈希算法, 同时还提供了 aes,blowfish,rc4,rsa,xtea 等加密算法

下例将计算输出数据的 sha1 和 md5 的校验值:

例 12.20 hash_sha1.go

在这里插入图片描述
在这里插入图片描述
sha1.New() 将创建一个新的 hash.Hash 对象, 之后可计算 SHA1 校验值,Hash 类型实际上是一个接口, 同时该接口又实现了 io.Writer 接口:
在这里插入图片描述
通过 io.WriteString 或 hasher.Write, 可计算特定字符串的校验值.

在这里插入图片描述