使用 Gob 以追加方式将日志写入文件

Mah*_*oni 5 binary encoding serialization go gob

是否可以使用 Gob 编码将结构串联附加到使用 append 的同一文件中?它适用于写作,但是当我不止一次使用解码器阅读时,我遇到了:

extra data in buffer
Run Code Online (Sandbox Code Playgroud)

所以我首先想知道这是否可行,或者我是否应该使用 JSON 之类的东西来逐行附加 JSON 文档。因为另一种方法是序列化一个切片,但再次将其作为一个整体读取将违背 append 的目的。

icz*_*cza 6

gob软件包不是设计用于这种方式使用的。gob 流必须由单个写入gob.Encoder,也必须由单个读取gob.Decoder

\n

这样做的原因是因为gob包不仅序列化您传递给它的值,它还传输数据来描述它们的类型:

\n
\n

一串碎片是自我描述的。流中的每个数据项前面都有其类型的规范,以一小组预定义类型的形式表示。

\n
\n

这是编码器/解码器\xe2\x80\x93关于什么类型以及如何传输的状态\xe2\x80\x93,后续的新编码器/解码器不会(无法)分析“前面的”流来重建相同的状态并从前一个编码器/解码器停止的地方继续。

\n

当然,如果您创建一个gob.Encoder,您可以使用它来序列化任意数量的值。

\n

您也可以创建一个gob.Encoder并写入一个文件,然后创建一个新的gob.Encoder,并附加到同一个文件,您必须使用 2gob.Decoder秒来读取这些值,与编码过程完全匹配。

\n

作为演示,让我们看一个例子。此示例将写入内存缓冲区 ( bytes.Buffer)。2 个后续编码器将写入它,然后我们将使用 2 个后续解码器来读取值。我们将写入该结构的值:

\n
type Point struct {\n    X, Y int\n}\n
Run Code Online (Sandbox Code Playgroud)\n

对于简短、紧凑的代码,我使用这个“错误处理程序”函数:

\n
func he(err error) {\n    if err != nil {\n        panic(err)\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

现在是代码:

\n
const n, m = 3, 2\nbuf := &bytes.Buffer{}\n\ne := gob.NewEncoder(buf)\nfor i := 0; i < n; i++ {\n    he(e.Encode(&Point{X: i, Y: i * 2}))\n}\n\ne = gob.NewEncoder(buf)\nfor i := 0; i < m; i++ {\n    he(e.Encode(&Point{X: i, Y: 10 + i}))\n}\n\nd := gob.NewDecoder(buf)\nfor i := 0; i < n; i++ {\n    var p *Point\n    he(d.Decode(&p))\n    fmt.Println(p)\n}\n\nd = gob.NewDecoder(buf)\nfor i := 0; i < m; i++ {\n    var p *Point\n    he(d.Decode(&p))\n    fmt.Println(p)\n}\n
Run Code Online (Sandbox Code Playgroud)\n

输出(在Go Playground上尝试):

\n
&{0 0}\n&{1 2}\n&{2 4}\n&{0 10}\n&{1 11}\n
Run Code Online (Sandbox Code Playgroud)\n

请注意,如果我们仅使用 1 个解码器来读取所有值(循环直到i < n + m,则当迭代达到 时,我们会收到您在问题中发布的相同错误消息n + 1,因为后续数据不是序列化的Point,而是新gob流的开始。

\n

因此,如果你想坚持使用该gob包来做你想做的事情,你必须稍微修改,增强你的编码/解码过程。你必须以某种方式标记界限(因此在解码时,您会知道必须创建一个新的解码器来读取后续值)。

\n

您可以使用不同的技术来实现此目的:

\n
    \n
  • 在继续写入值之前,您可以写出一个数字,一个计数,这个数字将告诉您使用当前编码器写入了多少个值。
  • \n
  • 如果您不想或无法知道当前编码器将写入多少个值,则当您不使用当前编码器写入更多值时,可以选择写出一个特殊的编码器结束值。当前编码器。解码时,如果遇到这种特殊的end-of-encoder值,您就会知道必须创建一个新的解码器才能读取更多值。
  • \n
\n

这里需要注意一些事项:

\n
    \n
  • gob套件最高效、最紧凑,因为每次创建和使用新编码器时,都必须重新传输类型规范,从而导致更多开销,并使编码/解码过程变慢。
  • \n
  • 您无法在数据流中查找,如果您从头读取整个文件直到获得所需的值,则只能解码任何值。请注意,即使您使用其他格式(例如 JSON 或 XML),这也有些适用。
  • \n
\n

如果您想要寻找功能,您需要管理索引文件,该文件将告诉新编码器/解码器在哪些位置开始,因此您可以寻找该位置,创建一个新解码器,并从那里开始读取值。

\n

警告

\n

gob.NewDecoder()文件表明:

\n
\n

如果 r 没有同时实现 io.ByteReader,它将被包装在 bufio.Reader 中。

\n
\n

这意味着,如果您使用os.Fileexample (它没有实现io.ByteReader),内部使用的bufio.Reader可能会从传递的读取器中读取比gob.Decoder实际使用的更多的数据(正如其名称所示,它执行缓冲 IO)。因此,在同一输入读取器上使用多个解码器可能会导致解码错误,因为bufio.Reader先前解码器内部使用的数据可能会读取不会使用的数据并传递到下一个解码器。

\n

对此的解决方案/解决方法是显式传递一个实现io.ByteReader不“提前”读取缓冲区的读取器。例如:

\n
type byteReader struct {\n    io.Reader\n    buf []byte\n}\n\nfunc (br byteReader) ReadByte() (byte, error) {\n    if _, err := io.ReadFull(br, br.buf); err != nil {\n        return 0, err\n    }\n    return br.buf[0], nil\n}\n\nfunc newByteReader(r io.Reader) byteReader {\n    return byteReader{r, make([]byte, 1)}\n}\n
Run Code Online (Sandbox Code Playgroud)\n

请参阅没有此包装器的错误示例:https://go.dev/play/p/dp1a4dMDmNc

\n

看看上面的包装器如何解决问题:https://go.dev/play/p/iw528FTFxmU

\n

检查一个相关问题:Efficient Go serialization of struct to disk

\n