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

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

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

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

やりたいこと

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

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

データ構造

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

Offset: 00 01 02 03 04 05 06 07
00 ファイル名サイズ
(4 byte 固定: BigEndian)
ファイルサイズ
(4 byte 固定: BigEndian)
08 ファイル名
(可変長: LittleEndian)
・・ ファイル内容
(可変長: LittleEndian)

データ例

  • ファイル名
    • HOGE.txt
  • ファイルサイズ
    • 8 byte
  • ファイル内容
    • 12345678
Offset: 00 01 02 03 04 05 06 07 データ値
00 00 00 00 08 00 00 00 08 0x00000008 0x00000008
08 48 4F 47 45 2E 74 78 74 H O G E . t x t
10 31 32 33 34 35 36 37 38 1 2 3 4 5 6 7 8

コマンド

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

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

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

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

ソースコード

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

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

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)
    }
}

まとめ

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

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

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

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

hiro_nico
見習いエンジニア Go / Perl など勉強中
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