除了 fmt 和 os 包之外, 还需要导入 bufio 包, 以实现带缓冲的输入和输出操作.
从键盘或标准输入端 (即 os.Stdin), 可读取用户输入, 当然最简单的方式则是采用 fmt 包中, 给出 Scan-或Sscan-前缀的函数, 如下:
Scanln 可从标准输入端中, 读取文本输入, 并将连续出现的文本 (以空格分隔), 放入到连续的形参中, 并在一个新行 (给出回车键) 出现时, 停止读取操作,Scanf 可实现与 Scanln 相同的功能, 但它的第一个形参为格式化字符串, 因此读取内容将放入到该字符串中, 而 Sscan 相关函数也可实现上述的类似功能, 但这类函数只能读取输入字符串 (而不是标准输入端), 同时还能检查读入元素的个数, 以及函数运行的错误. 在以下示例中, 将使用一个来自于 bufio 包的读取器 (包含缓冲).
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 语句, 来测试键盘输入:
Go 语言的文件是一个 os.File 类型的指针, 也被称为文件句柄, 在 *os.File 文件类型中, 还可使用标准输入端(os.Stdin) 和输出端 (os.Stdout), 如下:
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 循环.
如果这是一个应用需求, 可使用 io/ioutil 包的 ioutil.ReadFile() 方法, 它可将读入的字节数据, 返回给
[]byte, 以及返回一个错误码, 同样 WriteFile() 方法也可将一个 []byte 数组, 写入到一个文件中.
2. 带缓冲的读取操作:
除了 ReadString() 之外, 还可使用一种更通用的方式来读取文件, 也就是不将文件视为多个文本行, 或是
一个二进制文件. 这时可使用 bufio.Reader 类型的 Read() 方法, 如下:
3. 从文件中读取每列数据:
如果在文件中, 使用了空格, 来分隔不同的数据列, 可使用 fmt 包中, 以 FScan 为前缀的函数, 在以下示
例中, 可将三列数据, 读入 v1,v2,v3 变量中, 之后会将读出的变量, 附加到一个 slice 中, 以便还原对应的
数据列.
path 包的 filepath 子包, 可提供文件名的跨平台处理函数, 比如 Base() 函数, 可返回路径的文件名, 且不包含路径分隔符 (/或).
来自标准库的 compress 包, 可提供 bzip2,flate,gzip,lzw,zlib 等格式的压缩文件的读取. 以下示例将给出 gzip文件的读取.
除了文件句柄之外, 还需要一个 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 的另一种替代方式:
os.Stdout.WriteString(”hello, world\n”) 可实现屏幕写入,
可创建并打开一个只写文件 test, 可能存在的错误码将被丢弃. 此处不会使用缓冲, 而是直接写入文件 (f.WriteString()).
使用 io 包的 Copy 函数, 可实现最简单的文件复制:
注意 defer 的用法, 当目的文件打开时, 出现了一个错误并返回, 这时将会执行 defer src.Close(), 如果省略该语句, 源文件将一直打开, 并会占用资源.
os 包提供了一个 os.Args 变量, 它的类型为字符串 slice, 并能用于命令行参数的处理, 当应用程序启动时, 命令行参数将被读取到上述变量中, 如下:
在编辑器或 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 函数中,又在命令行参数之间, 加入了一个空格.
flag 包可对命令行参数进行解析, 通常用于替换一些常量, 比如在某些情况下, 需要为命令行的常量参数, 提供一个不同的数值, 在 flag 包中, 提供了一种结构类型 Flag:
在以下示例中, 模拟了 Unix 系统的 echo 工具:
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 节的示例.
在以下示例中, 将组合使用文件的缓冲读取和命令行标记的解析, 即使未输入命令行参数, 也会输出相应的信息. 如果给出文件名, 且文件存在, 将会显示文件内容, 测试命令为 cat test.
在处理 IO 缓冲时, 使用 slice 存储, 是一种标准的 Go 编程方式, 在以下的 cat 示例中, 将使用一个 for 循环,把文件读入一个 slice 缓冲中, 并会将读取内容, 写入到标准输出端.
在 cat2.go 示例中, 将使用 os.file 和 os 包的 Read 方法, 并会使用与上例相同的解决方案:
defer 关键字可保证打开文件的自动关闭 (在函数返回之前), 如下:
以下示例将给出 io 操作中的接口用法:
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.
通过网络传输或是文件存储进行数据传递时, 必须使用编码和解码操作, 目前已出现了大量的编码格式, 比如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 结构, 同时在本书的示例中, 为了代码的简洁, 都省略了错误的处理, 但在实际的程序中, 必须谨慎处理程序中出现的错误.
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 或文件的读取和写入.
一个 XML 的编码片段如下:
与 json 包类似,xml 包可提供了 Marshal() 和 UnMarshal() 函数, 用于 XML 数据的编码和解码, 但它可使用一种更通用的方式, 进行文件的读取和写入 (或是使用其他类型实现的 io.Reader 和 io.Writer).
与 json 相同,xml 数据也可实现与数据结构之间的编码和解码, 参见 15.8 节的示例. 在 encoding/xml 包中, 还实现了一个简单的 xml 解析器 (SAX), 它可读取 XML 数据并进行解析. 以下示例展示了该解析器的用法:
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 类型, 如果转换成字符串, 则更容易理解.
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 实现通用处理操作.
以下将给出一个简单的编解码示例, 这里将使用字节缓冲, 来模拟网络传输.
下例可将编码数据直接写入文件:
基于网络的数据传输, 必须进行加密, 以使黑客无法读取或修改这些数据, 而数据发送时得到的校验值, 与数据接收后算出的校验值必须一致, 另外在 Go 语言的标准库中, 已有超过 30 个包, 可用于网络传输.
• hash 包: 实现了 adler32,crc32,crc64 和 fnv 等校验方式
• crypto 包: 实现了 md4,md5,sha1 等哈希算法, 同时还提供了 aes,blowfish,rc4,rsa,xtea 等加密算法
下例将计算输出数据的 sha1 和 md5 的校验值:
sha1.New() 将创建一个新的 hash.Hash 对象, 之后可计算 SHA1 校验值,Hash 类型实际上是一个接口, 同时该接口又实现了 io.Writer 接口:
通过 io.WriteString 或 hasher.Write, 可计算特定字符串的校验值.