必要に迫られて、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.BigEndian
か binary.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などもありません。