新しいことにはウェルカム

技術 | 電子工作 | ガジェット | ゲーム のメモ書き

Go言語でファイル・IO・ストリームを使った読み書き方法のメモ

Go言語は、シンプルな関数を組み合わせてプログラミングをすることが多く、また、関数毎にエラーチェックが発生するので、全般的にコードが長くなりがちです。

ちょっとした事をしたい時でも、コードをそれなりに書く必要があるのですが、毎回ゼロから書くのは面倒なので、よくやる処理を自分コピペ用にメモしておこうと思います。

ここでは、Go言語で、ファイル(IO)を使って読み書きする方法の自分用メモを記載しました。

はじめに

ioについて

Go言語では、入出力はioパッケージとして抽象化されていて、ioパッケージを使って読み書きプログラムを記述します。

ファイルはio.Readerio.Writerインターフェイスを持っているので、ioを使って記載された読み書きコードはそのまま使えます。

また、ファイルに限らず、他の入出力であっても、io.Readerio.Writerインターフェイスを持っていれば、そのまま使えます。

byteについて

ioでのデータのやり取りは、全てbyteのスライスを介して行われます。

例えば、何かしらのデータをioで読み込む時、読み込み元のデータはbyteのスライス、つまり[]byteで持っています。

そして、そのデータの一部をio.Reader.Read()で取得するのですが、取得したデータもbyteのスライスです。

また、ioでデータの書き込みはio.Writer.Write()で行うのですが、書き込むデータもbyteのスライスです。

ファイル読み込み(一番ローレベルな読み込み方法)

基本的な流れ

  • ファイルを開いてFileを取得します
  • Fileio.Readerインターフェイスを持っています
  • io.ReaderRead()メソッドを持っています
  • Read()は引数に渡したbyteスライスにデータを書き込み、書き込んだバイト数を返します

終端処理の注意

  • ある読み込みで終端になった場合、読み込み数がゼロ以上かつerr==EOFerr!=nil)となる場合があります
  • 終端はerr==EOFになります
  • 読み込みバイト数がゼロの時、終端であることもありますが、必ずしも終端を示すものではありません

なので、読み込み処理は下記が必要になります

  • エラーチェックの前に、読み込み数をチェックして、ゼロ以上だと読み込み処理をする
  • 終端チェックはerr==EOFで行う
f, err := os.Open("read_data.txt") // ファイルを読み取りで開く
defer f.Close() // 開いたファイルは閉じる
if err != nil {
    return err
}

const bufferSize = 256 // 読み取りバッファーサイズ
var content []byte
buffer := make([]byte, bufferSize) // 読み取りバッファー

for {
    n, err := f.Read(buffer)
    if 0 < n {
        // 読み込み処理
        content = append(content, buffer...)
    }
    if err == io.EOF {
        break // 終端
    }
    if err != nil {
        return nil, err // エラー
    }
}

fmt.Println(string(content))

ファイル読み込み(全部まとめて書き込み)

io.Read()を使うと、中身をちょっとづつ読んでいく形になるのですが、ioutil.readAll()を使うと、io.Readerが持っているデータを一気に全て読み込んでくれます。

f, err := os.Open("read_data.txt")
defer f.Close()
if err != nil {
    return err
}

content, err := ioutil.ReadAll(f) // 全部読み込んでくれる
if err != nil {
    return err
}

fmt.Println(string(content))

ファイル読み込み(ファイルオープンから全てまとめて)

対象がファイルの場合、ioutil.ReadFile()で、ファイルオープンから読み込み、そしてファイルのクローズまで全てやってくれます。

content, err := ioutil.ReadFile("read_data.txt")
if err != nil {
    return err
}
fmt.Println(string(content))

これが一番楽ですね。

ファイル書き込み

io.Writer.Write()で書き込みます。

f, err := os.Create("write_data.txt")
defer f.Close()
if err != nil {
    return err
}

content := "TEST\ntest\nテスト\nてすと"

_, err = f.Write([]byte(content))
if err != nil {
    return err
}

ファイル書き込み(バッファ)

書き込む量が少ない時は上記で問題ないのですが、大量のデータを書き込んだ時など、エラーで失敗する時があります。

そんな時は、データを分けてちょっとづつ書き込む必要があるのですが、bufioを使えば、その分けて書き込む作業を良しなにしてくれます。

書き込みサイズが明確でない時は、直接io.Writer.Write()で書き込みせず、bufioを介して行う方が安全です。

使い方

bufioは、既存のio.Writerをラップするio.Writerを作成し、そのio.Writerに対して書き込みすると、書き込みをバッファーサイズに分割して、 既存のio.Writerに書き込みしてくれます。

バッファーサイズまでデータが溜まったら、溜まった分を書き込むので、書き込みデータの最後まで来たら、Writer.Flash()で、バッファーに溜まっていた残りデータを書き込む必要があります。

f, err := os.Create("write_data.txt")
defer f.Close()
if err != nil {
    return err
}

fw := bufio.NewWriter(f)
content := "TEST\ntest\nテスト\nてすと"

_, err = fw.Write([]byte(content))
if err != nil {
    return err
}

err = fw.Flush()
if err != nil {
    return err
}

ファイル書き込み(ファイルオープンから全てまとめて)

対象がファイルの場合、ioutil.WriteFile()で、ファイルオープンから書き込み、そしてファイルのクローズまで、全てやってくれます。

content := "TEST\ntest\nテスト\nてすと"

err := ioutil.WriteFile("write_data.txt", []byte(content), 0644)
if err != nil {
    return err
}

これが一番楽ですね。

ファイルから1行づつ読み込み

1回のio.Readerからのデータの読み込みで読み込まれるデータの量は不定です。しかし、データを1行づつ読み込みたいということがあります。

前述のbufioには、io.Readerからデータを1行づつ取得する機能があります。

io.ReaderからScannerを作成します。

ScannerScan()で1行取得し、取得できるとtrueとなり、データはText()で持ってくることができます。

終端またはエラーになるとScan()falseとなり、エラーはErr()で確認します。

f, err := os.Open("read_data.txt")
defer f.Close()
if err != nil {
    return err
}

fr := bufio.NewScanner(f)

for fr.Scan() {
    fmt.Println(fr.Text())
}

if err := fr.Err(); err != nil {
    return err
}

メモリ上にioを作る

データの入出力先をメモリにするにはbytes.Bufferを使います。

bytes.Bufferには、データを格納しておく[]byteのメモリ領域があり、io.Readerio.Writerインターフェースを持っていて、 インターフェースを介してのメモリへのデータの読み書きができます。

データは当然io.Readerで取り出せますし、Bytes()で全て直接取得することもできます。

var b bytes.Buffer

b.Write([]byte("TEST")) // io.Writer で書き込み

content, err := ioutil.ReadAll(&b) // io.Reader で読み込み
if err != nil {
    return err
}
fmt.Println(string(content)) // TEST

fmt.Println(string(b.Bytes())) // TEST データを直接取得

また、bytes.NewBuffer()で、bytes.Bufferの作成とデータの初期設定を同時に行うこともできます。

b := bytes.NewBuffer([]byte("TEST"))

content, err := ioutil.ReadAll(b) // io.Reader
if err != nil {
    return err
}
fmt.Println(string(content)) // TEST

fmt.Println(string(b.Bytes())) // TEST

注意点

bytes.Bufferはメモリデータへの読み書きをするため、io.Readerio.Writerインターフェースはbytes.Bufferポインターに対して定義されています。

なので、bytes.Bufferioに渡すには、bytes.Bufferのポインターを渡す必要があります。

前述のvar b bytes.Bufferはオブジェクトなので&bで渡すのに対し、b := bytes.NewBuffer()はポインターなのでbで渡しているのはこのためです。

io.Reaerio.Writerを直接つなぐ(ストリーム)

io.Readerからデータを読み取り、そのままio.Writerに書き込みたい時があります。

その場合、全てのデータを受け取ってからデータを出力すると、全てのデータを格納するメモリ領域が必要になるので効率が悪くなります。

そこで、Read()で部分的に読み取って、それをWrite()で書き込むという作業を、io.ReaderがEOFになるまで続けるようにすれば、メモリはRead()で読み取った分だけで済むので、 効率がよくなります。いわゆるストリーム処理というやつです。

ストリーム処理のコードはわざわざ書く必要はなく、io.Copy()でその作業をやってくれます。

ファイルを開いて別名保存

r, err := Open("read_data.txt")
if err != nil{
    return err
}
defer r.Close()

w, err := Create("write_data.txt")
if err != nil{
    return err
}
defer w.Close()

// rから読み取ってwに書き出すのを、rのデータが最後になるまでやってくれる
_, err = io.Copy(w, r)
if err != nil{
    return err
}

io.Writerio.Readerを直接つなぐ(パイプ)

io.Writeにデータを書き込んで、その書き込まれたデータを、io.Readerを引数に取る関数に渡したい時は、io.Pipe()を使います。

例を書こうかと思ったのですが、あまり使うシチュエーションがなく、必要になった時の参照用に、関連情報のURLだけ記しておきます。

感想など

対象のデータがあって、それをio.Readerio.Writer部分的に読み書きする、ストリームをイメージするとしっくりきますね。

データ操作や変換するのに、io.Readerio.Writerのラッパーのio.Readerio.Writerを作成して使うことがあるのですが、これもストリームの一種とも言えます。

一旦データを全て読み込んでから次の処理をしたい衝動をグッとこらえて、io.Readerio.Writerのままで渡してストリーム処理するようにすると、多くの並行処理が走らせれるようになるので、Go言語ではio.Readerio.Writerは積極的にそのままで使っていった方がいいのかなぁと思います。

例えば、ファイルをzipにするのを、ストリームで処理すると下記のような感じになります。

fr, err := os.Open("from.txt")
if err != nil {
    return err
}
defer fr.Close()

fw, err := os.Create("to.zip")
if err != nil {
    return err
}
defer fw.Close()

z := zip.NewWriter(fw)
if err != nil {
    return err
}
defer z.Close()

fz, err := z.Create("from.txt")
if err != nil {
    return err
}

_, err = io.Copy(fz, fr)
if err != nil {
    return err
}

このように書いておけば、リソースの消費は少ないので、多数のファイルを処理したくなった時は、ファイル毎にゴルーチンを実行するようにすると、多数の処理を並行実行させるといったことができるようになります。