Go语言自定义二进制文件的读写操作

 
虽然Go语言的 encoding/gob 包非常易用,而且使用时所需代码量也非常少,但是我们仍有可能需要创建自定义的二进制格式。自定义的二进制格式有可能做到最紧凑的数据表示,并且读写速度可以非常快。

不过,在实际使用中,我们发现以Go语言二进制格式的读写通常比自定义格式要快非常多,而且创建的文件也不会大很多。但如果我们必须通过满足 gob.GobEncoder 和 gob.GobDecoder 接口来处理一些不可被 gob 编码的数据,这些优势就有可能会失去。

在有些情况下我们可能需要与一些使用自定义二进制格式的软件交互,因此了解如何处理二进制文件就非常有用。

写自定义二进制文件

Go语言的 encoding/binary 包中的 binary.Write() 函数使得以二进制格式写数据非常简单,函数原型如下:

func Write(w io.Writer, order ByteOrder, data interface{}) error

Write 函数可以将参数 data 的 binary 编码格式写入参数 w 中,参数 data 必须是定长值、定长值的切片、定长值的指针。参数 order 指定写入数据的字节序,写入结构体时,名字中有_的字段会置为 0。

下面通过一个简单的示例程序来演示一下 Write 函数的使用,示例代码如下:
package main

import (
    "bytes"
    "encoding/binary"
    "fmt"
    "os"
)

type Website struct {
    Url int32
}

func main() {
    file, err := os.Create("output.bin")
    for i := 1; i <= 10; i++ {
        info := Website{
            int32(i),
        }
        if err != nil {
            fmt.Println("文件创建失败 ", err.Error())
            return
        }
        defer file.Close()

        var bin_buf bytes.Buffer
        binary.Write(&bin_buf, binary.LittleEndian, info)
        b := bin_buf.Bytes()
        _, err = file.Write(b)

        if err != nil {
            fmt.Println("编码失败", err.Error())
            return
        }
    }
    fmt.Println("编码成功")
}
运行上面的程序会在当前目录下生成 output.bin 文件,文件内容如下:

0100 0000 0200 0000 0300 0000 0400 0000
0500 0000 0600 0000 0700 0000 0800 0000
0900 0000 0a00 0000 

读自定义二进制文件

读取自定义的二进制数据与写自定义二进制数据一样简单。我们无需解析这类数据,只需使用与写数据时相同的字节顺序将数据读进相同类型的值中。

示例代码如下:
package main

import (
    "bytes"
    "encoding/binary"
    "fmt"
    "os"
)

type Website struct {
    Url int32
}

func main() {
    file, err := os.Open("output.bin")
    defer file.Close()
    if err != nil {
        fmt.Println("文件打开失败", err.Error())
        return
    }
    m := Website{}
    for i := 1; i <= 10; i++ {
        data := readNextBytes(file, 4)
        buffer := bytes.NewBuffer(data)
        err = binary.Read(buffer, binary.LittleEndian, &m)
        if err != nil {
            fmt.Println("二进制文件读取失败", err)
            return
        }
        fmt.Println("第", i, "个值为:", m)
    }
}

func readNextBytes(file *os.File, number int) []byte {
    bytes := make([]byte, number)

    _, err := file.Read(bytes)
    if err != nil {
        fmt.Println("解码失败", err)
    }

    return bytes
}
运行结果如下:

go run main.go
第 1 个值为: {1}
第 2 个值为: {2}
第 3 个值为: {3}
第 4 个值为: {4}
第 5 个值为: {5}
第 6 个值为: {6}
第 7 个值为: {7}
第 8 个值为: {8}
第 9 个值为: {9}
第 10 个值为: {10}

至此,我们完成了对自定义二进制数据的读和写操作。只要小心选择表示长度的整数符号和大小,并将该长度值写在变长值(如切片)的内容之前,那么使用二进制数据进行工作并不难。

Go语言对二进制文件的支持还包括随机访问。这种情况下,我们必须使用 os.OpenFile() 函数来打开文件(而非 os.Open()),并给它传入合理的权限标志和模式(例如 os.O_RDWR 表示可读写)参数。

然后,就可以使用 os.File.Seek() 方法来在文件中定位并读写,或者使用 os.File.ReadAt() 和 os.File.WriteAt() 方法来从特定的字节偏移中读取或者写入数据。

Go语言还提供了其他常用的方法,包括 os.File.Stat() 方法,它返回的 os.FileInfo 包含了文件大小、权限以及日期时间等细节信息。