Go バイナリファイルの読み書き

# Go バイナリファイルの読み書き

今回は、Goで固定長と可変長なデータがあるバイナリファイルを読み書きしてみたいと思います。

## やりたいこと

複数個ファイルを1つのバイナリファイルにする。(結合)

バイナリファイルを複数ファイルに書き戻す。(分割)

## データ構造

下記のデータ構造を1つのファイルとして扱い、連結していく。

<table>
<tr>
<th>Offset:</th>
<th>00</th>
<th>01</th>
<th>02</th>
<th>03</th>
<th>04</th>
<th>05</th>
<th>06</th>
<th>07</th>
</tr>
<tr>
<th>00</th>
<td colspan="4">
ファイル名サイズ<br>
(4 byte 固定: BigEndian)
</td>
<td colspan="4">
ファイルサイズ<br>
(4 byte 固定: BigEndian)
</td>
</tr>
<tr>
<th>08</th>
<td colspan="8">
ファイル名<br>
(可変長: LittleEndian)
</td>
</tr>
<tr>
<th>・・</th>
<td colspan="8">
ファイル内容<br>
(可変長: LittleEndian)
</td>
</tr>
</table>

### データ例

* ファイル名
 * `HOGE.txt`
* ファイルサイズ
 * 8 byte
* ファイル内容
 * `12345678`

<table>
<tr>
<th>Offset:</th>
<th>00</th>
<th>01</th>
<th>02</th>
<th>03</th>
<th>04</th>
<th>05</th>
<th>06</th>
<th>07</th>
<th colspan="8">データ値</th>
</tr>
<tr>
<th>00</th>
<td>00</td>
<td>00</td>
<td>00</td>
<td>08</td>
<td>00</td>
<td>00</td>
<td>00</td>
<td>08</td>
<td colspan="4">0x00000008</td>
<td colspan="4">0x00000008</td>
</tr>
<tr>
<th>08</th>
<td>48</td>
<td>4F</td>
<td>47</td>
<td>45</td>
<td>2E</td>
<td>74</td>
<td>78</td>
<td>74</td>
<td>H</td>
<td>O</td>
<td>G</td>
<td>E</td>
<td>.</td>
<td>t</td>
<td>x</td>
<td>t</td>
</tr>
<tr>
<th>10</th>
<td>31</td>
<td>32</td>
<td>33</td>
<td>34</td>
<td>35</td>
<td>36</td>
<td>37</td>
<td>38</td>
<td>1</td>
<td>2</td>
<td>3</td>
<td>4</td>
<td>5</td>
<td>6</td>
<td>7</td>
<td>8</td>
</tr>
</table>

## コマンド

↓ 2つのファイルを1つのファイルにまとめる

```shell:結合時
$ sample.exe join text.txt text.md
joined.dat
```

↓ 1つのファイルから2つのファイルを作る

```shell:分割時
$ sample.exe split joined.dat
text.txt
text.md
```

## ソースコード

そしてザクっと作ったものがこちら。

結合時のファイル名とかは、固定にしてます。

```go:sample.go
package main

import (
	"bytes"
	"encoding/binary"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"os"
	"path/filepath"
)

type head struct {
	FileNameSize uint32
	FileSize     uint32
}

func main() {
	// 引数が足りない
	if len(os.Args) < 2 {
		os.Exit(1)
	}

	// 引数1つ目はコマンドとして扱う
	switch os.Args[1] {
	case `join`:
		dist := `joined.dat`
		// 書き込み先のファイルを作成
		wf, err := os.Create(dist)
		if err != nil {
			log.Fatal(err)
		}
		defer wf.Close()

		for _, file := range os.Args[2:] {
			b, err := ioutil.ReadFile(file)
			if err != nil {
				log.Fatal(err)
			}

			// ファイルパスをファイル名のみに変更
			filename := filepath.Base(file)

			head := head{
				FileNameSize: uint32(len(filename)),
				FileSize:     uint32(len(b)),
			}
			// 固定長部分を書き込み
			err = binary.Write(wf, binary.BigEndian, &head)
			if err != nil {
				log.Fatal(err)
			}
			// データ部分を書き込み
			_, err = wf.Write(append([]byte(filename), b...))
			if err != nil {
				log.Fatal(err)
			}
		}
		fmt.Println(dist)
	case `split`:
		for _, file := range os.Args[2:] {
			b, err := ioutil.ReadFile(file)
			if err != nil {
				log.Fatal(err)
			}

			buf := bytes.NewReader(b)

			for {
				// 固定長部分を読み取る
				head := head{}
				err := binary.Read(buf, binary.BigEndian, &head)
				if err == io.EOF {
					break
				} else if err != nil {
					log.Fatal(err)
				}

				// ファイル名を読み取る
				filename := make([]byte, head.FileNameSize)
				err = binary.Read(buf, binary.LittleEndian, &filename)
				if err != nil {
					log.Fatal(err)
				}

				// データを読み取る
				data := make([]byte, head.FileSize)
				err = binary.Read(buf, binary.LittleEndian, &data)
				if err != nil {
					log.Fatal(err)
				}

				// ファイルに出力する
				if err := ioutil.WriteFile(string(filename), data, 0666); err != nil {
					log.Fatal(err)
				}
				fmt.Println(string(filename))
			}
		}
	default:
		// コマンドが違う
		os.Exit(1)
	}
}

```

## まとめ

固定長のところは、構造体を作って読み書き。

可変長部分のところは、
書くときは 何も考えずつなげて 吐き出す。
読むときは データ長のスライスを作って 読み込む。

という感じでコードを書いてみました。

良い書き方があったら教えてくださいネ～。