mainvisual

golangを初めて学びだすと、1つの動作を実現しようとすると、多種多様な書き方が存在して、 それをどのように組み合わせれば、一番ベストなのか、非常に迷いました

ドキュメントを見ても、より良い情報が得られず、情報収集は非常に難航しました

まだ、比較的新しい言語であるので、ブログや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
      • 入力全体を読み込む
      • ファイル全体を読み込む
      • テンポラリファイルの作成

本文

読み込み書き込みする方法と手順

まず、golangでI/O関連の処理を理解しようと思ったら、 その前にインターフェースについて理解している必要があります。 IOのストリームの取り扱いはこのインターフェースによって実現されているからです。

golangのインターフェースについては、本記事の趣旨とは外れますので、他のサイトを参考にして下さい。 javaのインターフェースを理解すると、golangの方も同様に理解できます。

ファイルや、標準入力などに書き込み読み込みを行なう流れはこのようになります。

  1. 書き込みする場合は、Writerインターフェース(Write(p []byte) (n int, err error)をもっていれば良い) 読み込みする場合は、Readerインターフェース(Read(p []byte) (n int, err error)をもっていればよい)を 実装したストリームを用意する
  2. bufio.NewReader, bufio.NewWriterを使って、 バッファリングを使用した高効率なioインターフェースを使ってストリームをラップする
  3. 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を発見していないけれども、バッファーザイズをオーバーした時に、 isPrefixtureとなり、それが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.Createos.Openos.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.Bufferbytes.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)
	}
}

引用: http://qiita.com/ono_matope/items/d5e70d8a9ff2b54d5c37

このようにすると、高速に動作します

効率よく読み書きをするライブラリ

ファイルへの書き込み読み込み の章で、ファイルへの読み書きを解説しました。
ですが、今の標準パッケージには、同様以上の機能が実装されています。 そこで、簡単にですが、そのパッケージの紹介をします。

主に次の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))
}

参考資料