Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

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

Hiraku
PHP, Go界隈をうろうろしています。最近はgRPCと戦ってる。 特に明示していなければ、記事中のソースコード片は `CC-0 1.0` とします。出典表示無しで自由にコピペして頂いて構いません。 ただ、記事自体をコピペされるのは嫌なので、ソースコード部分以外の文章は通常通り全ての著作権を私が保持するものとします。 引用を超える範囲のコピペは止めて下さい。
http://blog.tojiru.net/
mercari
フリマアプリ「メルカリ」を、グローバルで開発しています。
https://tech.mercari.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした