LoginSignup
48
27

More than 3 years have passed since last update.

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

Posted at

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

48
27
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
48
27