Posted at

Goで固定長フォーマットを扱う

必要に迫られて、Goでバイナリの読み書きをしているのですが、encoding/binaryに関する解説が少ない気がしたのでまとめます。


固定長フォーマットとは

なんかこう、「先頭から 4byte, 2byte, 10byteという風に区切って、最初の4byteがA, 次の2byteはビッグエンディアンでuint32扱いでB, 次の10byteは文字列…」みたいに、一切の説明が省かれたフォーマットのことを指して発言しています。

当然、元データをparseして、構造体とかに変換してからプログラムで扱いたくなると思います。ただの[]byteのまま扱うとか地獄すぎですよね。

type Foo struct{

A [4]byte
B uint32
C uint16
D uint16
}

func ParseBinary(in []byte) *Foo {
// ...
}

// 実行イメージ

func TestParseBinary(t *testing.T) {
b := []byte{
0x12, 0x34, 0x56, 0x78, // この辺はAの領域
0x12, 0x34, 0x56, 0x78, // この辺はBの領域
0x12, 0x34, // ここはCの領域
0x56, 0x78, // ここはDの領域
}

// なんか関数を通すと
got := ParseBinary(b)

// 上から順番に、いい感じにぶった切ってFoo構造体に詰めてくれたらいいのに!
want := &Foo{
A: [...]byte{0x12, 0x34, 0x56, 0x78},
B: 0x12345678,
C: 0x1234,
D: 0x5678,
}

if !reflect.DeepEqual(got, want) {
t.Fatalf("got: %v, want: %v", got, want)
}
}


結論から言うと

encoding/binaryがとても便利です。


[]byteを読んでは詰める

binary.Readを使うと簡単に詰められます。

func Read(r io.Reader, order ByteOrder, data interface{}) error

単純な使い方としては、uint32とか、uint64とかに詰めることです。

package main

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

func main() {
var i uint64
buf := bytes.NewReader([]byte{0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x90})
if err := binary.Read(buf, binary.BigEndian, &i); err != nil {
fmt.Println("binary.Read failed:", err)
}
fmt.Printf("0x%x\n", i)// 0x1234567812345678
}

intやfloatなどの数値系は、第二引数に binary.BigEndianbinary.LittleEndian を指定することで、エンディアンを指示できます。この例だと、uint64とは8byteなので、先頭から8byte分だけ読み取ってツメツメしています。(9バイト目の 0x90 は読み込まれていない)

もちろん binary.LittleEndian を指定すると、ひっくり返って 0x7856341278563412 になります。

binary.Readのすごいのは、structを渡すと、上から順番に詰めるというような芸当ができることです。

ツメツメするためにはそれぞれのフィールドに長さが設定されている必要があります。byteの配列を使ってフィールドを宣言しましょう。

ちょろっと書き換えて↓みたいにすると、

type Foo struct{

A [4]byte
B [4]byte
C uint8
}

func main() {
var i Foo
buf := bytes.NewReader([]byte{0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x90})
if err := binary.Read(buf, binary.BigEndian, &i); err != nil {
fmt.Println("binary.Read failed:", err)
}
fmt.Printf("%x\n", i) // {12345678 12345678 90}
}

ちゃんと構造体のフィールドの定義順にツメツメしてくれてます! 便利!


スライスは使えないの?

ちなみにこんな風に、配列でなく[]byteみたいにスライスにはできないのか?と思うかもしれません。

type Foo struct{

A []byte
B []byte
C uint8
}

こちらはうまく動きません。

単体のスライス、 []byte の場合は、makeで初期化しておくと動きました。capではなくlenを見ており、 make([]byte, 9) とかは大丈夫ですが、 make([]byte, 0, 9) みたいなのは長さ0なので何も読み込んでくれません。

//これなら動く

i := make([]byte, 9)
buf := bytes.NewReader([]byte{0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x90})
if err := binary.Read(buf, binary.BigEndian, &i); err != nil {
fmt.Println("binary.Read failed:", err)
}
fmt.Printf("%x\n", i) // 123456781234567890

ドキュメントによると、以下の2つだけが許可されているとのこと。


Data must be a pointer to a fixed-size value or a slice of fixed-size values.



  • 固定長のvalueへのポインタ

  • 固定長のvalueのスライス

なのでフィールドが全部固定長でできているstructならどんな構造でもうまく取り扱ってくれます。

スライスのスライスや、スライスを含むstructは駄目ってことですね。


固定長フォーマットで構造体を書き出す

ご想像の通り、ReadがあるならWriteもあります。

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

さっきとは逆回しにFoo構造体を渡すと、io.Writerに追記していってくれます。これも便利。


まとめ

encoding/binary便利です。

ちなみに、encoding一族に位置しているパッケージではありますが、趣旨がgoのネイティブの変数との相互変換なので、encoding/json や encoding/xml などとはインターフェースの流儀が異なります。 Marshal/Unmarshalなどもありません。