Go言語は、シンプルな関数を組み合わせてプログラミングをすることが多く、また、関数毎にエラーチェックが発生するので、全般的にコードが長くなりがちです。
ちょっとした事をしたい時でも、コードをそれなりに書く必要があるのですが、毎回ゼロから書くのは面倒なので、よくやる処理を自分コピペ用にメモしておこうと思います。
ここでは、Go言語で、ファイル(IO)を使って読み書きする方法の自分用メモを記載しました。
はじめに
ioについて
Go言語では、入出力はio
パッケージとして抽象化されていて、io
パッケージを使って読み書きプログラムを記述します。
ファイルはio.Reader
・io.Writer
インターフェイスを持っているので、io
を使って記載された読み書きコードはそのまま使えます。
また、ファイルに限らず、他の入出力であっても、io.Reader
・io.Writer
インターフェイスを持っていれば、そのまま使えます。
byteについて
io
でのデータのやり取りは、全てbyte
のスライスを介して行われます。
例えば、何かしらのデータをio
で読み込む時、読み込み元のデータはbyte
のスライス、つまり[]byte
で持っています。
そして、そのデータの一部をio.Reader.Read()
で取得するのですが、取得したデータもbyte
のスライスです。
また、io
でデータの書き込みはio.Writer.Write()
で行うのですが、書き込むデータもbyte
のスライスです。
ファイル読み込み(一番ローレベルな読み込み方法)
基本的な流れ
- ファイルを開いて
File
を取得します File
はio.Reader
インターフェイスを持っていますio.Reader
はRead()
メソッドを持っていますRead()
は引数に渡したbyteスライスにデータを書き込み、書き込んだバイト数を返します
終端処理の注意
- ある読み込みで終端になった場合、読み込み数がゼロ以上かつ
err==EOF
(err!=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
を作成します。
Scanner
はScan()
で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.Reader
・io.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.Reader
・io.Writer
インターフェースはbytes.Buffer
のポインターに対して定義されています。
なので、bytes.Buffer
をio
に渡すには、bytes.Buffer
のポインターを渡す必要があります。
前述のvar b bytes.Buffer
はオブジェクトなので&b
で渡すのに対し、b := bytes.NewBuffer()
はポインターなのでb
で渡しているのはこのためです。
io.Reaer
とio.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.Writer
とio.Reader
を直接つなぐ(パイプ)
io.Write
にデータを書き込んで、その書き込まれたデータを、io.Reader
を引数に取る関数に渡したい時は、io.Pipe()
を使います。
例を書こうかと思ったのですが、あまり使うシチュエーションがなく、必要になった時の参照用に、関連情報のURLだけ記しておきます。
感想など
対象のデータがあって、それをio.Reader
・io.Writer
で部分的に読み書きする、ストリームをイメージするとしっくりきますね。
データ操作や変換するのに、io.Reader
・io.Writer
のラッパーのio.Reader
・io.Writer
を作成して使うことがあるのですが、これもストリームの一種とも言えます。
一旦データを全て読み込んでから次の処理をしたい衝動をグッとこらえて、io.Reader
・io.Writer
のままで渡してストリーム処理するようにすると、多くの並行処理が走らせれるようになるので、Go言語ではio.Reader
・io.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 }
このように書いておけば、リソースの消費は少ないので、多数のファイルを処理したくなった時は、ファイル毎にゴルーチンを実行するようにすると、多数の処理を並行実行させるといったことができるようになります。