Go语言自定义数据文件

对一个程序非常普遍的需求包括维护内部数据结构,为数据交换提供导入导出功能,也支持使用外部工具来处理数据。

由于我们这里的关注重点是文件处理,因此我们纯粹只关心如何从程序内部数据结构中读取数据并将其写入标准和自定义格式的文件中,以及如何从标准和自定义格式文件中读取数据并写入程序的内部数据结构中。

本节中,我们会为所有的例子使用相同的数据,以便直接比较不同的文件格式。所有的代码都来自 invoicedate 程序(在 invoicedata 目录中的 invoicedata.go > gob.go、inv.go、jsn.go、txt.go 和 xml.go 等文件中)。大家可以从我的网盘(链接: https://pan.baidu.com/s/1j22QfIScihrauVCVFV6MWw 提取码: ajrk)下载相关的代码。

该程序接受两个文件名作为命令行参数,一个用于读,另一个用于写(它们必须是不同的文件)。程序从第一个文件中读取数据(以其后缀所表示的任何格式),并将数据写入第二个文件(也是以其后缀所表示的任何格式)。

由 invoicedata 程序创建的文件可跨平台使用,也就是说,无论是什么格式,Windows 上创建的文件都可在 Mac OS X 以及 Linux 上读取,反之亦然。Gzip 格式压缩的文件(如 invoices.gob.gz)可以无缝读写。

这些数据由一个 []invoice 组成,也就是说,是一个保存了指向 Invoice 值的指针的切片。每一个发票数据都保存在一个 invoice 类型的值中,同时每一个发票数据都以 []*Item 的形式保存着 0 个或者多个项。
type Invoice struct {
    Id          int
    Customerld  int
    Raised      time.Time
    Due         time.Time
    Paid        bool
    Note        string
    Items       []*Item
}

type Item struct {
    Id       st ring
    Price    float64
    Quantity int
    Note     string
}
这两个结构体用于保存数据。下表给出了一些非正式的对比,展示了每种格式下读写相同的 50000 份随机发票数据所需的时间,以及以该格式所存储文件的大小。

计时按秒计,并向上舍入到最近的十分之一秒。我们应该把计时结果认为是无绝对单位的,因为不同硬件以及不 同负载情况下该值都不尽相同。大小一栏以千字节(KB)算,该值在所有机器上应该都是相同的。

对于该数据集,虽然未压缩文件的大小千差万别,但压缩文件的大小都惊人的相似。而代码的 函数不包括所有格式通用的代码(例如,那些用于压缩和解压缩以及定义结构体的代码)。

表:各种格式的速度以及大小对比
后缀 读取 写入 大小(KiB) 读/写LOC 格式
.gob 0.3 0.2 7948 21 + 11 =32 Go二进制
.gob.gz 0.5 1.5 2589
jsn 4.5 2.2 16283 32+17 = 49 JSON
.jsn.gz 4.5 3.4 2678
.xml 6.7 1.2 18917 45 + 30 = 75 XML
.xml.gz 6.9 2.7 2730
..txt 1.9 1.0 12375 86 + 53 = 139 纯文本(UTF-8)
.txt.gz 2.2 2.2 2514
.inv 1.7 3.5 7250 128 + 87 = 215 自定义二进制
.inv.gz 1.6 2.6 2400

这些读写时间和文件大小在我们的合理预期范围内,除了纯文本格式的读写异常快之外。这得益于 fmt 包优秀的打印和扫描函数,以及我们设计的易于解析的自定义文本格式。

对于 JSON 和 XML 格式,我们只简单地存储了日期部分而非存储默认的 time.Time 值(一个 ISO-8601 日期/时间字符串),通过牺牲一些速度和增加一些额外代码稍微减小了文件的大小。

例如,如果让JSON代码自己来处理time.Time值,它能够运行得更快,并且其代码行数与 Go语言二进制编码差不多。

对于二进制数据,Go语言的二进制格式是最便于使用的。它非常快且极端紧凑,所需的代码非常少,并且相对容易适应数据的变化。然而,如果我们使用的自定义类型不原生支持 gob 编码,我们必须让该类型满足 gob.Encoder 和 gob. Decoder 接口,这样会导致 gob 格式的 读写相当得慢,并且文件大小也会膨胀。

对于可读的数据,XML 可能是最好使用的格式,特别是作为一种数据交换格式时非常有用。与处理 JSON 格式相比,处理 XML 格式需要更多行代码。这是因为 Go [没有一个 xml.Marshaler 接口,也因为我们这里使用了并行的数据类型 (XMLInvoice 和 XMLItem)来帮助映射 XML 数据和发票数据(invoice 和 Item)。

使用 XML 作为外部存储格式的应用程序可能不需要并行的数据类型或者也不需要 invoicedata 程序这样的 转换,因此就有可能比 invoicedata 例子中所给出的更快,并且所需的代码也更少。

除了读写速度和文件大小以及代码行数之外,还有另一个问题值得考虑:格式的稳健性。例如,如果我们为 Invoice 结构体和 Item 结构体添加了一个字段,那么就必须再改变文件的格式。我们的代码适应读写新格式并继续支持读旧格式的难易程度如何?如果我们为文件格式定义版本,这样的变化就很容易被适应(会以本章一个练习的形式给岀),除了让 JSON 格式同时适应读写新旧格式稍微复杂一点之外。

除了 Invoice 和 Item 结构体之外,所有文件格式都共享以下常量:
const (
    fileType        = "INVOICES"        //用于纯文本格式
    magicNumber     = 0xl25D            // 用于二进制格式
    fileVersion     = 100               //用于所有的格式
    dataFormat      = "2006-01-02"      //必须总是使用该日期
)
magicNumber 用于唯一标记发票文件。fileVersion 用于标记发票文件的版本,该标记便于之后修改程序来适应数据格式的改变。dataFormat 稍后介绍,它表 示我们希望数据如何按照可读的格式进行格式化。

同时,我们也创建了一对接口。
type InvoiceMarshaler interface {
    Marshallnvoices(writer io.Writer, invoices []*Invoice) error
}
type InvoiceUnmarshaler interface {
    Unmarshallnvoices(reader io.Reader) ([]*Invoice, error)
}
这样做的目的是以统一的方式针对特定格式使用 reader 和 writer。例如,下列函数是 invoicedata 程序用来从一个打开的文件中读取发票数据的。
func readinvoices(reader io.Reader, suffix string)([]*Invoice, error) {
    var unmarshaler InvoicesUnmarshaler
    switch suffix {
        case ".gobn:
            unmarshaler = GobMarshaler{}
        case H.inv":
            unmarshaler = InvMarshaler{}
        case ,f. jsn", H. jsonn:
            unmarshaler = JSONMarshaler{}
        case ".txt”:
            unmarshaler = TxtMarshaler{}
        case ".xml":
            unmarshaler = XMLMarshaler{}
    }
    if unmarshaler != nil {
        return unmarshaler.Unmarshallnvoices(reader)
    }
    return nil, fmt.Errorf("unrecognized input suffix: %s", suffix)
}
其中,reader 是任何能够满足 io.Reader 接口的值,例如,一个打开的文件 ( 其类型为 *os . File)> 一个 gzip 解码器 ( 其类型为 *gzip. Reader) 或者一个 string. Readero 字符串 suffix 是文件的后缀名 ( 从 .gz 文件中解压之后)。

在接下来的小节中我们将会看到 GobMarshaler 和 InvMarshaler 等自定义的类型,它们提供了 MarshmlTnvoices() 和 Unmarshallnvoices() 方法 (因此满足 InvoicesMarshaler 和 InvoicesUnmarshaler 接口)。