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

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

是否可以使用 Gob 编码将结构串联附加到同一文件中?它适用于写作,但当我多次使用解码器阅读时,我 运行 进入:

extra data in buffer

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

gob package wasn't designed to be used this way. A gob stream has to be written by a single gob.Encoder, and it also has to be read by a single 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.

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

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

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

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

type Point struct {
    X, Y int
}

为了简短、紧凑的代码,我使用这个 "error handler" 函数:

func he(err error) {
    if err != nil {
        panic(err)
    }
}

现在代码:

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 上尝试):

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

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

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

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

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

这里有几点需要注意:

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

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

查一个相关问题:

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

  1. 像往常一样创建文件编码 gob(首先编码写入 headers)
  2. 关闭文件
  3. 打开文件进行 追加
  4. 使用中间编写器编码虚拟结构(写入 headers)
  5. 重置写入器
  6. 照常编码 gob(不写入 headers)

示例:

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)
    }
}

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

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
}