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

vatpfxk5  于 2023-05-20  发布在  Go
关注(0)|答案(3)|浏览(129)

有没有可能使用Gob编码,通过append将结构体串联追加到同一个文件中?它适用于写作,但当阅读解码器不止一次我遇到:

extra data in buffer

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

vjhs03f7

vjhs03f71#

gob软件包并不是为这种方式设计的。一个gob流必须由一个gob.Encoder写入,也必须由一个gob.Decoder读取。
这是因为gob包不仅序列化你传递给它的值,它还传输数据来描述它们的类型:
一串垃圾是自我描述的。流中的每个数据项前面都有其类型的规范,以一小组预定义类型的形式表示。
这是编码器/解码器的状态-关于什么类型以及它们如何被传输,随后的新编码器/解码器将不会(不能)分析“先前的”流以重构相同的状态并且在先前的编码器/解码器停止的地方继续。
当然,如果创建一个gob.Encoder,可以使用它来序列化任意多的值。
你也可以创建一个gob.Encoder并写入一个文件,然后再创建一个新的gob.Encoder,并附加到同一个文件中,但是你必须使用2个gob.Decoder来读取这些值,这与编码过程完全匹配。
作为演示,让我们跟随一个例子。本示例将写入内存中的缓冲区(bytes.Buffer)。2个后续的编码器将写入它,然后我们将使用2个后续的解码器来读取值。我们将写这个结构的值:

type Point struct {
    X, Y int
}

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

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}

请注意,如果我们只使用一个解码器来读取所有值(循环直到i < n + m,当迭代到达n + 1时,我们会得到与您在问题中发布的相同的错误消息,因为后续数据不是序列化的Point,而是新gob流的开始。
因此,如果你想坚持使用gob包来做你想做的事情,你必须稍微修改,* 增强 * 你的编码/解码过程。当使用新的编码器时,你必须以某种方式标记边界(所以当解码时,你会知道你必须创建一个新的解码器来读取后续的值)。
您可以使用不同的技术来实现这一点:

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

这里需要注意的一些事情:

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

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

警告

gob.NewDecoder()文件说明:
如果r没有实现io.ByteReader,它将被 Package 在bufio. Reader中。
这意味着,如果你使用os.File(它没有实现io.ByteReader),内部使用的bufio.Reader可能会从传递的读取器读取比gob.Decoder实际使用的更多的数据(正如它的名字所说,它做缓冲IO)。因此,在同一个输入读取器上使用多个解码器可能会导致解码错误,因为前一个解码器的内部使用的bufio.Reader可能会读取不会被使用的数据并传递给下一个解码器。
对此的一个解决方案是显式传递一个实现io.ByteReader的读取器,该读取器不会“提前”读取缓冲区。例如:

type byteReader struct {
    io.Reader
    buf []byte
}

func (br byteReader) ReadByte() (byte, error) {
    if _, err := io.ReadFull(br, br.buf); err != nil {
        return 0, err
    }
    return br.buf[0], nil
}

func newByteReader(r io.Reader) byteReader {
    return byteReader{r, make([]byte, 1)}
}

请看一个没有这个 Package 器的错误示例:https://go.dev/play/p/dp1a4dMDmNc
看看上面的wrapper是如何解决这个问题的:https://go.dev/play/p/iw528FTFxmU
查看相关问题:将结构体高效地序列化到磁盘

0s7z1bwu

0s7z1bwu2#

除了上面的,我建议使用一个中间结构来排除gob头:

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

xvw2m8pv3#

除了伟大的icza答案,你可以使用以下技巧来附加到一个gob文件与已经写入的数据:当附加第一次写入丢弃第一编码时:
1.像往常一样创建文件Encode gob(首先对写头进行编码)
1.关闭文件
1.打开文件进行 append
1.使用和中间编写器编码伪结构(写入标头)
1.重置编写器
1.像往常一样对gob进行编码(不写入头)
示例:

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

相关问题