golangのファイルへの読み書きのまとめ
golangである動作を実現しようとするとしても、 たくさんの書き方があってどれが一番合ってベストなのかわかりませんでした。
ドキュメントを見ても良い情報が得られず、情報を収集するのが苦しかったです。
まだ、比較的新しい言語であるので、ブログやQiitaなど、情報が少ないのが原因だと思います
そこで、情報のまとめを発信することで、自分の情報整理と同時に、これからgolangを学ぶ人、 また、今まさに学ぼうとしている人にとっての助けとなるようにしようと考え、 ここにI/O関連の取り扱いや使い方についてまとめようと思います
目次
- 読み込み書き込みする方法と手順
- ファイルへの書き込み読み込み
- os.Create, os.NewFile, os.Open, os.OpenFileの違い
- 標準入力/標準出力/標準エラーへの書き込み読み込み
- メモリへデータへの書き込み読み込み
- bytes.Buffer / bytes.Reader
- Buffer/Readerの主な使用用途
- 効率よく読み書きをするライブラリ
- bufio
- 標準入力を行単位で読み込む
- bufio.Reader/bufio.Writer
- bufio.Writerの問題点
- io/ioutil
- 入力全体を読み込む
- ファイル全体を読み込む
- テンポラリファイルの作成
- bufio
本文
読み込み書き込みする方法と手順
まず、golangでI/O関連の処理を理解しようと思ったら、 その前にインターフェースについて理解している必要があります。 IOのストリームの取り扱いはこのインターフェースによって実現されているからです。
golangのインターフェースについては、本記事の趣旨とは外れますので、他のサイトを参考にして下さい。 javaのインターフェースを理解すると、golangの方も同様に理解できます。
ファイルや、標準入力などに書き込み読み込みを行なう流れはこのようになります。
- 書き込みする場合は、Writerインターフェース(
Write(p []byte) (n int, err error)
をもっていれば良い) 読み込みする場合は、Readerインターフェース(Read(p []byte) (n int, err error)
をもっていればよい)を 実装したストリームを用意する bufio.NewReader
,bufio.NewWriter
を使って、 バッファリングを使用した高効率なioインターフェースを使ってストリームをラップするbufio.ReadString
,bufio.WriteString
などを使って、読み書きを行なう
javaで言うと、
- Writerインターフェース => FileWriterクラス
- bufio.NewWriter => BufferedWriterクラス
と同じようなものだと思ってもらって大丈夫です
また、何かのプログラムを組み場合には、ファイルから1行読み込むなんてよくあります。 golangでも、そのような面倒なことを書かなくてもいいように、 あらかじめ最適化された関数を提供しています
例えば、以下の様な方法があります
bufio.NewScanner
を使うioutil.ReadAll
を使うioutil.ReadFile
を使うioutil.WriteFile
を使う
これらも、本記事で紹介します
ファイルへの書き込み読み込み
ファイルへ読み込みや書き込みを行なうには、os.File
を作成し、
bufio.NewReader
またはbufio.NewWriter
でバッファリングするものでラップして、使用します
os.File
を作るには様々な方法がありますが、
Rubyやpythonといった言語と同じ動作をさせるには次のようなものを使います
os.OpenFile(filename, os.O_RDONLY, 0644)
:
読み込み専用os.OpenFile(filename, os.O_WRONLY | os.O_CREATE, 0644)
:
書き込み専用(ファイル先頭からバイトが書き換えられる)os.OpenFile(filename, os.O_WRONLY | os.O_CREATE | os.O_APPEND, 0644)
:
追記モード(最後尾に追加)os.OpenFile(filename, os.O_RDWR, 0644)
:
読み書き可(O_RDONLYとO_WRONLYをあわせもつ)os.Create(filename)
:
新規ファイル作成(ただし、同じ名前のファイルは上書きされる)
読み込みの例
package main
import (
"bufio"
"fmt"
"log"
"os"
)
// Readln returns a single line (without the ending \n)
// from the input buffered reader.
// An error is returned iff there is an error with the
// buffered reader.
func Readln(r *bufio.Reader) (string, error) {
var (
isPrefix bool = true
err error = nil
line, ln []byte
)
for isPrefix && err == nil {
line, isPrefix, err = r.ReadLine()
ln = append(ln, line...)
}
return string(ln), err
}
func newFile(fn string) *os.File {
fp, err := os.OpenFile(fn, os.O_RDONLY, 0644)
if err != nil {
log.Fatal(err)
}
return fp
}
func main() {
fp := newFile("/path/to/file")
defer fp.Close()
reader := bufio.NewReader(fp)
str, err := Readln(reader)
for err == nil {
fmt.Println(str)
str, err = Readln(reader)
}
}
ここで注意したいのは、Readln(r *bufio.Reader) (string, error)
内部で使用している
isPrefix
という変数に注目して下さい
ReadLine
関数によって、\n
を区切りとして、1行を読み込みます。
ですが、これは、1行が4096byte以下の長さであった場合のみです。
それ以上は次にReadLine
が呼ばれた時に、読み出されます
ですので、\n
を発見していないけれども、バッファーザイズをオーバーした時に、
isPrefix
がture
となり、それが1行の続きであることがわかるのです
書き込みの例
package main
import (
"os"
"bufio"
"log"
)
func newFile(fn string) *os.File {
fp, err := os.OpenFile(fn, os.O_WRONLY | os.O_APPEND, 0644)
if err != nil {
log.Fatal(err)
}
return fp
}
func main(){
fp := newFile("/path/to/file")
defer fp.Close()
writer := bufio.NewWriter(fp)
writeString := "hogehoge hogehoge string"
_, err := writer.WriteString(writeString)
if err != nil {
log.Fatal(err)
}
writer.Flush()
}
書き込みでは、writer.Flush()
に注目してください。
WriteString
を行っただけだと、まだそのデータはメモリ上に存在します。
なぜなら、高効率なIOを行なうために、まとめて書き出すためです
必ず、目的のデータが書き込み終わったら、writer.Flush()
を呼び出して、
メモリ内のデータを保存します
os.Create, os.Open, os.OpenFile, os.NewFileの違い
os.Create
とos.Open
はos.OpenFile
のラッパーです。
それぞれの実装は以下のようになっていて、とても単純だということがわかります
go/src/os/file.go
func Open(name string) (file *File, err error) {
return OpenFile(name, O_RDONLY, 0)
}
func Create(name string) (file *File, err error) {
return OpenFile(name, O_RDWR | O_CREATE| O_TRUNC, 0666)
}
標準入力/標準出力/標準エラーへの書き込み読み込み
標準入力や標準出力に読み書きを行うときは、os.Stdin
,os.Stdout
を使用します。
これから得られるのは、\*os.File
型なので、以下のように使用することができます。
fp := os.Stdin
defer fp.Close()
reader := bufio.NewReader(fp)
標準出力にかきこむ際にも、同様にします
fp := os.Stdout
defer fp.Close()
writer := bufio.NewWriter(fp)
メモリのデータをあたかもファイルのように扱う
ファイル入出力などを行う関数のユニットテストを書く場合などに、 ファイルの代わりにメモリからデータを読んだり逆にファイルの代わりに メモリにデータを書き込んだりしたい場合があります。
そんな時に使える機能を提供しています。
bytes.Buffer / bytes.Reader
この2つには以下のような違いがあります。
bytes.Buffer
: (読み書き両方可)bytes.Reader
: (読み込み専用)
この、これを使用することのできるユーティリティとしては、以下のようなものがあります。
io.Reader
io.ReaderAt
io.WriterTo
io.Seeker
io.ByteScanner
io.RuneScanner
Buffer/Readerの主な使用用途
bytes.Buffer
とbytes.Reader
の主な使用用途は、ファイルの入出力をテストする場合です。
時々、文字列の結合にbytes.Buffer
を用いたりすることもできますが、
これを使うよりも、キャパシティ指定付き[]byte
を使ったほうが、10倍ほど高速なので、
使用方法は見なおしたほうが良さそうです。
func BenchmarkCapByteArray___(b *testing.B) {
for i := 0; i < b.N; i++ {
var m2 = make([]byte, 0, 100)
for _, v := range m {
m2 = append(m2, v...)
m2 = append(m2, ',')
}
_ = string(m2)
}
}
このようにすると、高速に動作します
効率よく読み書きをするライブラリ
ファイルへの書き込み読み込み の章で、ファイルへの読み書きを解説しました。
ですが、今の標準パッケージには、同様以上の機能が実装されています。
そこで、簡単にですが、そのパッケージの紹介をします。
主に次の2つがあります
- bufio
- io/ioutil
bufio
io
パッケージでは低レベルな入出力を提供しているため、
効率化するためにはバッファを使用したり、どこまで読み込んだかを管理したりと、
いろいろな手順を踏まなければなりませんでした。
bufio
では、入出力処理にバッファ処理の機能を付加し、
簡単に取り扱うための機能がサポートされています
標準入力を行単位で読み込む
関数bufio.NewScanner
は、io.Reader
を引数にとり、bufio.NewScanner
型を生成します。
この、bufio.NewScanner
型は、改行を区切りとして、
入力をバッファリングしつつ文字列を読み出します
次の例は、Scanner
を使って、文字列を読み込んでいます
package main
import (
"bufio"
"fmt"
"log"
"os"
)
func main() {
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
fmt.Println("input text>>" + scanner.Text())
}
if err := scanner.Err(); err != nil {
log.Fatal(err)
}
}
bufio.Reader/bufio.Writer
io.Reader
, io.Writer
ではとても汎用的な入出力機能を提供しています。
しかしながら、バッファリング機能などは実装されておらず、入出力の効率化を望もうとすれば、
バッファリングは欠かせません。
バッファリングを行なうことで、「必ず速くなる!」 とは言えませんが、
入出力文字数が多くなれば、効果を発揮しまし、io.Reader
, io.Writer
と、
同様のインターフェースを提供しているので、使わない手は無いでしょう。
bufio.Reader/bufio.Writer
にはバッファサイズを指定できます。
バッファサイズを大きくした場合には、一度に取り込める文字数が多くなり、
一般的に効率は上がりますが、メモリの使用量も比例するため、必ずしも大きい方がいいとは限りません。
バッファサイズのデフォルトでは4096byteであり、普通の使用用途では、変更する必要は無いでしょう。
// readerはio.Reader型、writerはio.Writer型を示す
br := bufio.NewReader(reader)
br := bufio.NewReaderSize(reader, 8192)
bw := bufio.NewWriter(writer)
bw := bufio.NewWriterSize(writer, 8192)
bufio.Writerの問題点
bufio.Writer
は、バッファリング機能を付加し、高効率な出力処理を提供します。
しかし、「ファイルを閉じた際に、自動的にデータが書き込まれない」という問題があります。
ですので、ファイルへの書き込みが終了した際には、データを強制的に書き込む必要があります。
強制的な書き込みには、bufio.Writer
型のメソッドFlash
を使用します。
fp, _ := os.OpenFile("/path/to/file", os.O_WRONLY | os.O_APPEND, 0644)
defer fp.close()
writer := bufio.NewWriter(fp)
bw := bufio.NewWriter(writer)
bw.WriteString("Hello world!")
bw.Flush()
io/ioutil
io/ioutil
には、ファイルの取り扱いととても簡単にしたユーティリティがあります。
bufio
のように1行ずつ読みこんだりして、メモリを節約したり、
そもそもファイル自体がそんなに大きく無い場合などに重宝します。
また、テンポラリファイル(一時ファイル)などの作成などの機能も提供しています。 しかし、今回は記事の趣旨から外れるため、省きます。
入力全体を読み込む
関数ioutil.ReadAll
は、io.Reader
型から得られるすべてのデータを読み込みます。
読み込んだデータは[]byte
型で返されます。
package main
import (
"fmt"
"io/ioutil"
"log"
"os"
)
func openFile(filename string) *os.File {
fp, err := os.OpenFile(filename, os.O_RDONLY, 0644)
if err != nil {
log.Fatal(err)
}
return fp
}
func readAll(fp *os.File) []byte {
bs, err := ioutil.ReadAll(fp)
if err != nil {
log.Fatal(err)
}
return bs
}
func main() {
fp := openFile("/path/to/file")
bs := readAll(fp)
fmt.Println(string(bs))
}
ファイル全体を読み込む
単に、ファイルからでーたを読み込むのであれば、ioutil.ReadFile
が使えます。
必要なのはファイル名だけですので、かなりシンプルに読み込めます。
package main
import (
"fmt"
"io/ioutil"
"log"
)
func main() {
bs, err := ioutil.ReadFile("/path/to/file")
if err != nil {
log.Fatal(err)
}
fmt.Println(string(bs))
}
もっと知りたいなと思ったときは!
その他、この記事でわからないことがあったら、teratial
というサービスを利用するといいと思います。
勇者が初歩的な質問からマニアックな質問に幅広く答えてくれますよ!
是非、一度使ってはいかがでしょうか?
参考資料
- http://mattn.kaoriya.net/software/lang/go/20140501172821.htm
- http://qiita.com/ikawaha/items/28186d965780fab5533d
- http://qiita.com/suin/items/7eb4fc405ac73846a9b1
- http://qiita.com/ymko/items/54d6f377d259fd8e3f49
- http://stackoverflow.com/questions/8757389/reading-file-line-by-line-in-go
- http://www.yunabe.jp/docs/golang_io.html
- https://github.com/astaxie/build-web-application-with-golang/blob/master/ja/07.5.md
- https://golang.org/pkg/bufio/
- https://golang.org/pkg/io/
- https://golang.org/pkg/io/ioutil/