【问题标题】:binary.Read does not handle struct padding as expectedbinary.Read 未按预期处理结构填充
【发布时间】:2023-03-17 01:11:01
【问题描述】:

在最近的一个 Go 项目中,我需要读取 Python 生成的二进制数据文件,但由于填充,Go 中的 binary.Read 无法正确读取。下面是我的问题的一个最小示例。

我处理的结构体如下格式

type Index struct{
    A int32
    B int32
    C int32
    D int64
}

如您所见,结构体的大小为 4+4+4+8=20,但 Python 添加了额外的 4 个字节用于对齐。所以大小实际上是 24。

下面是我用来编写这个结构的可运行 Python 代码:

#!/usr/bin/env python
# encoding=utf8

import struct

if __name__ == '__main__':
    data = range(1, 13)
    format = 'iiiq' * 3
    content = struct.pack(format, *data)
    with open('index.bin', 'wb') as f:
        f.write(content)

iiiq 格式表示结构体中有 3 个 32 位整数和 1 个 64 位整数,这与我之前定义的 Index 结构体相同。运行此代码将生成一个名为index.bin 的文件,大小为 72,等于 24 * 3。

下面是我用来阅读index.bin的Go代码:

package main

import (
        "encoding/binary"
        "fmt"
        "os"
        "io"
        "unsafe"
)

type Index struct {
        A int32
        B int32
        C int32
        D int64
}

func main() {
        indexSize := unsafe.Sizeof(Index{})
        fp, _ := os.Open("index.bin")
        defer fp.Close()
        info, _ := fp.Stat()
        fileSize := info.Size()
        entryCnt := fileSize / int64(indexSize)
        fmt.Printf("entry cnt: %d\n", entryCnt)

        readSlice := make([]Index, entryCnt)
        reader := io.Reader(fp)
        _ = binary.Read(reader, binary.LittleEndian, &readSlice)
        fmt.Printf("After read:\n%#v\n", readSlice)
}

这是输出:

entry cnt: 3
After read:
[]main.Index{main.Index{A:1, B:2, C:3, D:17179869184}, main.Index{A:0, B:5, C:6, D:7}, main.Index{A:8, B:0, C:9, D:47244640266}}

从 Python 生成的文件中读取时,显然输出是混乱的。

所以我的问题是,如何在 Go 中正确读取 python 生成的文件(带填充)?

【问题讨论】:

  • 这个golang.org/ref/spec#Size_and_alignment_guarantees 可能会有所帮助。
  • @alex 除非我严重误解了某些东西,否则这里的内存布局无关紧要,因为binary.Read 只是按顺序读取结构的元素。
  • 也许这个play.golang.org/p/bCfWmKTP25 会有所帮助。
  • Python 的 struct library 与 C 类型兼容,而不是 Go 类型。如链接文档中所述,使用standard 对齐进行独立于平台的打包。
  • 抱歉,我没有正确阅读问题。但总结一下我的答案:Go struct memory layout 是由语言规范指定的。所以你可以让它成为你想要的任何东西。用额外的字段填充(如下所述)听起来是个不错的计划。

标签: struct go padding


【解决方案1】:

例如,

package main

import (
    "bufio"
    "encoding/binary"
    "fmt"
    "io"
    "os"
)

type Index struct {
    A int32
    B int32
    C int32
    D int64
}

func readIndex(r io.Reader) (Index, error) {
    var index Index
    var buf [24]byte
    _, err := io.ReadFull(r, buf[:])
    if err != nil {
        return index, err
    }
    index.A = int32(binary.LittleEndian.Uint32(buf[0:4]))
    index.B = int32(binary.LittleEndian.Uint32(buf[4:8]))
    index.C = int32(binary.LittleEndian.Uint32(buf[8:12]))
    index.D = int64(binary.LittleEndian.Uint64(buf[16:24]))
    return index, nil
}

func main() {
    f, err := os.Open("index.bin")
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        return
    }
    defer f.Close()
    r := bufio.NewReader(f)
    indexes := make([]Index, 0, 1024)
    for {
        index, err := readIndex(r)
        if err != nil {
            if err == io.EOF {
                break
            }
            fmt.Fprintln(os.Stderr, err)
            return
        }
        indexes = append(indexes, index)
    }
    fmt.Println(indexes)
}

输出:

[{1 2 3 4} {5 6 7 8} {9 10 11 12}]

输入:

00000000  01 00 00 00 02 00 00 00  03 00 00 00 00 00 00 00  |................|
00000010  04 00 00 00 00 00 00 00  05 00 00 00 06 00 00 00  |................|
00000020  07 00 00 00 00 00 00 00  08 00 00 00 00 00 00 00  |................|
00000030  09 00 00 00 0a 00 00 00  0b 00 00 00 00 00 00 00  |................|
00000040  0c 00 00 00 00 00 00 00                           |........|

【讨论】:

  • 我很好奇io.ReadAtLeast(r, buf[:], len(buf)) vs io.ReadFull(r, buf[:]) 是否有原因?此上下文中的语义是相同的,但后者在这里更具可读性。
  • @DaveC: ReadFullReadAtLeast 的特殊形式。固定。
【解决方案2】:

@Barber 的解决方案是可行的,但我发现添加填充字段不太舒服。而且我找到了更好的方法。

以下是完美运行的新 golang 读取代码:

package main

import (
    "fmt"
    "os"
    "io"
    "io/ioutil"
    "unsafe"
)

type Index struct {
    A int32
    B int32
    C int32
    // Pad int32
    D int64
}

func main() {
    indexSize := unsafe.Sizeof(Index{})
    fp, _ := os.Open("index.bin")
    defer fp.Close()
    info, _ := fp.Stat()
    fileSize := info.Size()
    entryCnt := fileSize / int64(indexSize)

    reader := io.Reader(fp)
    allBytes, _ := ioutil.ReadAll(reader)
    readSlice := *((*[]Index)(unsafe.Pointer(&allBytes)))
    realLen := len(allBytes) / int(indexSize)
    readSlice = readSlice[:realLen]
    fmt.Printf("After read:\n%#v\n", readSlice)
}

输出:

After read:
[]main.Index{main.Index{A:1, B:2, C:3, D:4}, main.Index{A:5, B:6, C:7, D:8}, main.Index{A:9, B:10, C:11, D:12}}

此解决方案不需要显式填充字段。

这里的本质是,如果让golang将整个字节转换成Indexstruct的切片,似乎可以很好地处理padding。

【讨论】:

  • 只要您对使用unsafe 感到满意,之所以这样称呼它是有原因的。不能保证 Go 的填充在机器或版本之间是一致的。您在这里依赖的事实是(在您的机器和 Go 版本上)Go 像文件一样填充其结构。
【解决方案3】:

你可以填充你的 Go 结构来匹配:

type Index struct {
    A int32
    B int32
    C int32
    _ int32
    D int64
}

产生:

[]main.Index{main.Index{A:1, B:2, C:3, _:0, D:4}, main.Index{A:5, B:6, C:7, _:0, D:8}, main.Index{A:9, B:10, C:11, _:0, D:12}}

binary.Read 知道跳过 _ 字段:

读入结构时,会跳过带有空白 (_) 字段名称的字段的字段数据;即,空白字段名称可用于填充。

(所以_0值不是因为文件中的填充设置为零,而是因为struct字段被初始化为0并且从未改变,并且文件中的填充被跳过而不是阅读。)

【讨论】:

  • 你能解释一下为什么在 D 之前填充吗?在发布之前我尝试在 D 之后填充,但值不正确。
  • 这就是 Python 填充其输出的方式,以便在 64 位边界上对齐。 4 << 3217179869184,这就是为什么你的第一个 D 没有先跳过填充就出现了。
猜你喜欢
  • 2021-05-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多