关于序列化:使用Gob以append方式将日志写入文件

Use Gob to write logs to a file in an append style

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

1
extra data in buffer

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

的目的


gob 包不是为这样使用而设计的。 gob 流必须由单个 gob.Encoder 写入,也必须由单个 gob.Decoder.

读取

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

A stream of gobs is self-describing. Each data item in the stream is preceded by a specification of its type, expressed in terms of a small set of predefined types.

这是编码器/解码器的状态——关于它们是什么类型以及它们是如何传输的——,后续的新编码器/解码器将不会(无法)分析"前面的"流以重建相同的状态并继续上一个编码器/解码器停止的地方。

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

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

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

1
2
3
type Point struct {
    X, Y int
}

简而言之,紧凑的代码,我使用这个"错误处理程序"函数:

1
2
3
4
5
func he(err error) {
    if err != nil {
        panic(err)
    }
}

现在是代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const n, m = 3, 2
buf := &bytes.Buffer{}

e := gob.NewEncoder(buf)
for i := 0; i < n; i++ {
    he(e.Encode(&Point{X: i, Y: i * 2}))
}

e = gob.NewEncoder(buf)
for i := 0; i < m; i++ {
    he(e.Encode(&Point{X: i, Y: 10 + i}))
}

d := gob.NewDecoder(buf)
for i := 0; i < n; i++ {
    var p *Point
    he(d.Decode(&p))
    fmt.Println(p)
}

d = gob.NewDecoder(buf)
for i := 0; i < m; i++ {
    var p *Point
    he(d.Decode(&p))
    fmt.Println(p)
}

输出(在 Go Playground 上试试):

1
2
3
4
5
&{0 0}
&{1 2}
&{2 4}
&{0 10}
&{1 11}

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

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

您可以使用不同的技术来实现这一点:

  • 在继续写入值之前,您可以写出一个数字、一个计数,这个数字将说明使用当前编码器写入了多少个值。
  • 如果您不想或无法确定将使用当前编码器写入多少个值,当您不使用当前编码器。解码时,如果遇到这种特殊的编码器结束值,您就会知道必须创建一个新的解码器才能读取更多值。

这里有一些注意事项:

  • 如果只使用单个编码器,gob 包是最高效、最紧凑的,因为每次创建和使用新编码器时,都必须重新传输类型规范,造成更多开销,并使编码 /解码过程较慢。
  • 您不能在数据流中查找,如果您从头开始读取整个文件直到您想要的值,您只能解码任何值。请注意,即使您使用其他格式(例如 JSON 或 XML),这在某种程度上也适用。

如果你想要寻找功能,你需要单独管理一个索引文件,它会告诉你新的编码器/解码器从哪个位置开始,所以你可以寻找那个位置,创建一个新的解码器,然后开始读取值从那里开始。

查看相关问题:Efficient Go serialization of struct to disk


除上述之外,我建议使用中间结构来排除gob header:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
package main

import (
   "bytes"
   "encoding/gob"
   "fmt"
   "io"
   "log"
)

type Point struct {
    X, Y int
}

func main() {
    buf := new(bytes.Buffer)
    enc, _, err := NewEncoderWithoutHeader(buf, new(Point))
    if err != nil {
        log.Fatal(err)
    }
    enc.Encode(&Point{10, 10})
    fmt.Println(buf.Bytes())
}


type HeaderSkiper struct {
    src io.Reader
    dst io.Writer
}

func (hs *HeaderSkiper) Read(p []byte) (int, error) {
    return hs.src.Read(p)
}

func (hs *HeaderSkiper) Write(p []byte) (int, error) {
    return hs.dst.Write(p)
}

func NewEncoderWithoutHeader(w io.Writer, sample interface{}) (*gob.Encoder, *bytes.Buffer, error) {
    hs := new(HeaderSkiper)
    hdr := new(bytes.Buffer)
    hs.dst = hdr

    enc := gob.NewEncoder(hs)
    // Write sample with header info
    if err := enc.Encode(sample); err != nil {
        return nil, nil, err
    }
    // Change writer
    hs.dst = w
    return enc, hdr, nil
}

func NewDecoderWithoutHeader(r io.Reader, hdr *bytes.Buffer, dummy interface{}) (*gob.Decoder, error) {
    hs := new(HeaderSkiper)
    hs.src = hdr

    dec := gob.NewDecoder(hs)
    if err := dec.Decode(dummy); err != nil {
        return nil, err
    }

    hs.src = r
    return dec, nil
}

除了很棒的 icza 答案之外,您还可以使用以下技巧将已写入数据的 gob 文件附加到 gob 文件中:当第一次附加时写入并丢弃第一个编码:

  • 像往常一样创建文件 Encode gob(首先编码写入标头)
  • 关闭文件
  • 打开文件进行追加
  • 使用和中间编写器编码虚拟结构(写入标头)
  • 重置作家
  • 像往常一样编码 gob(不写标题)
  • 示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    package main

    import (
       "bytes"
       "encoding/gob"
       "fmt"
       "io"
       "io/ioutil"
       "log"
       "os"
    )

    type Record struct {
        ID   int
        Body string
    }

    func main() {
        r1 := Record{ID: 1, Body:"abc"}
        r2 := Record{ID: 2, Body:"def"}

        // encode r1
        var buf1 bytes.Buffer
        enc := gob.NewEncoder(&buf1)
        err := enc.Encode(r1)
        if err != nil {
            log.Fatal(err)
        }

        // write to file
        err = ioutil.WriteFile("/tmp/log.gob", buf1.Bytes(), 0600)
        if err != nil {
            log.Fatal()
        }

        // encode dummy (which write headers)
        var buf2 bytes.Buffer
        enc = gob.NewEncoder(&buf2)
        err = enc.Encode(Record{})
        if err != nil {
            log.Fatal(err)
        }

        // remove dummy
        buf2.Reset()

        // encode r2
        err = enc.Encode(r2)
        if err != nil {
            log.Fatal(err)
        }

        // open file
        f, err := os.OpenFile("/tmp/log.gob", os.O_WRONLY|os.O_APPEND, 0600)
        if err != nil {
            log.Fatal(err)
        }

        // write r2
        _, err = f.Write(buf2.Bytes())
        if err != nil {
            log.Fatal(err)
        }

        // decode file
        data, err := ioutil.ReadFile("/tmp/log.gob")
        if err != nil {
            log.Fatal(err)
        }

        var r Record
        dec := gob.NewDecoder(bytes.NewReader(data))
        for {
            err = dec.Decode(&r)
            if err == io.EOF {
                break
            }
            if err != nil {
                log.Fatal(err)
            }
            fmt.Println(r)
        }
    }