Go言語 ファイル操作 まとめ

自分はリスキリングとして、Go言語の勉強を本格的に取り組んで、一ヶ月に満たない初心者だ。この記事は、学んだ事が、頭に定着するように、整理して書き残すものである。ここまで解った事として、Go言語を使いこなせるかどうかは、Go言語仕様を理解はもとより、Go言語のパッケージについての知識が大切だと感じた。そこで、設定ファイルの読み込みやログ出力、バッチ処理の基本となるファイル操作について必要な複数のパッケージについて整理を試みた。そのため、特定のパッケージの全機能を網羅するものではない。


## ファイル操作に有用なパッケージ

ファイル操作に活用されるパッケージは、以下の３つがある。これらを組み合わせて、目的の処理を記述すなければらなない。ファイル操作の開始時に必要なOpenとして、C言語では open, fopen の２系統があるが、Go言語では osパッケージに実装された open だけだ。これにより取得されたFile型変数を、bufio と io　で利用することになる。

* [os パッケージ](https://pkg.go.dev/os) ファイルのOpen/Close, Read,Writeなど  
* [bufio パッケージ](https://pkg.go.dev/bufio) バッファリングと行単位の読み書きなど
* [io パッケージ](https://pkg.go.dev/io) コピー、パイプ、全読み取りなど

それぞれのパッケージの概要を確認して、具体的なコードの基本的な組み立てを見ていく。本題に集中するために、import文やエラー処理は省略する。 基本的に import ブロックには、os,bufio,io,fmtが必要とみなして良い。


### os パッケージ https://pkg.go.dev/os
パッケージ os は、プラットフォームに依存しないオペレーティング システム機能へのインターフェイスを提供する。失敗した呼び出しは、エラー番号ではなくエラー型の値を返します。多くの場合、エラー内でより多くの情報を入手できる。たとえば、Open や Stat などのファイル名を使用する呼び出しが失敗した場合、エラーには出力時に失敗したファイル名が含まれ、タイプ *PathError になる。

本記事で扱った os パッケージ関数
* func Create(name string) (*File, error)
* func Open(name string) (*File, error)
* func (f *File) Close() error
* func (f *File) Read(b []byte) (n int, err error)
* func (f *File) Write(b []byte) (n int, err error)
* func (f *File) Stat() (FileInfo, error)


### bufio パッケージ https://pkg.go.dev/bufio
パッケージ bufio はバッファリングされた I/O を実装する。これは io.Reader または io.Writer オブジェクトをラップし、インターフェイスを実装する別のオブジェクト (Reader または Writer) を作成するが、バッファリングとテキスト I/O のヘルプを提供する。

本記事で扱った bufio の関数
* func NewScanner(r io.Reader) *Scanner
* func (s *Scanner) Scan() bool
* func (s *Scanner) Text() string
* func (s *Scanner) Err() error
* func NewReader(rd io.Reader) *Reader
* func (b *Reader) ReadLine() (line []byte, isPrefix bool, err error)
* func (b *Reader) ReadString(delim byte) (string, error)
* func NewWriter(w io.Writer) *Writer
* func (b *Writer) Write(p []byte) (nn int, err error)
* func (b *Writer) WriteString(s string) (int, error)

fmt パッケージでの利用も可能
* func Fprint(w io.Writer, a ...any) (n int, err error)
* func Fprintf(w io.Writer, format string, a ...any) (n int, err error)
* func Fprintln(w io.Writer, a ...any) (n int, err error)


### io パッケージ https://pkg.go.dev/io
パッケージ io は、I/O プリミティブへの基本的なインターフェイスを提供する。その主な仕事は、そのようなプリミティブの既存の実装 (パッケージ os にあるものなど) を、機能を抽象化する共有パブリック インターフェイスとその他の関連するプリミティブにラップすること。

これらのインターフェイスとプリミティブは、さまざまな実装で低レベルの操作をラップするため、別の方法で通知されない限り、クライアントは並列実行に対して安全であると想定してはいけない。

* func ReadAll(r Reader) ([]byte, error)


## 実装パターン
以下に、一般的に利用されそうな処理パターンを組み立てるコードを整理してみた。


### ファイルから、確保したメモリのサイズだけ読み取り

普段利用することは少ないが、プログラムで確保したメモリのサイズだけ読んで、順次処理することができる。
この方法は、固定レコード長のデータを読むことに適する。しかし、データとデータの境目や区切りを認識しないので、設定ファイルなどを読むことに適さない。ファイル全部を読むにはループを回す。また、特定のレコード等の位置を読みたい時は、file.Seek()を利用する。

~~~
byteData := make([]byte, 200)        // メモリに領域を確保
file, err := os.Open(filename)       // ファイルを開く
n, err := file.Read(byteData)        // バッファのサイズだけメモリへ取り込む
~~~

nは実際に読み取れたバイト数



### ファイルを一括で読み込む

JSON, YAML, XML 等で記述された設定ファイルを読んで、構造体へ変換する時に便利な方法。
ファイルのサイズが解らない時に、メモリの確保も同時に実施してくれるためだ。
確保可能なメモリサイズを超えないように、


~~~
file , err := os.Open(filename)      // ファイルを開く
stat := file.Stat()                  // サイズなどを読む、その後、サイズ判定など
byteData, err := io.ReadAll(file)    // ファイル全体をメモリへ読み込む
~~~


### 改行コード毎に読み込む

データの前処理などで、テキストファイルを１行づつ読み込んで、マッチした文字列単位に処理するのに適した方法。
`func NewScanner(r io.Reader) *Scanner`により、テキストファイルを順番に読むことができる。scanner.Text()の読み取り結果には、改行コードは含まれない。

~~~
file , err := os.Open(filename)
scanner := bufio.NewScanner(file)　 // スキャナ型を作成
for scanner.Scan() {                // テキスト行がなくなるまで、ループする
    line := scanner.Text()          // テキスト １行を読み取る
    // テキスト一行ごとの処理
}
err := scanner.Err()                // ループから抜けた原因をチェック
~~~

異なる改行コードを読む場合は、Split によって、スキャナの分割機能を設定できる。デフォルトの分割関数は ScanLines である。



### 行単位に読み込む

NewReaderは、デフォルトのバッファのサイズに合わせて、一行を読み取る。もし、バッファより一行が長い場合は、reader.ReadLine()のisPrefixにtrueがセットされる。残りは次のreader.ReadLine()コールで返され、行末まで読めたかは、isPrefixで判定できる。
バッファのサイズを知るには、`func (b * Reader ) Size() int` を用いる。注意点として、ReadLine から返されるテキストには、行末 ("\r\n" または "\n") は含まれない。

~~~
file, err := os.Open(filename)
reader := bufio.NewReader(file)
for {
   line, isPrefix, err := reader.ReadLine()
   if err == io.EOF {
      break
   }
   // エラー処理、また、isPrefixがfalseの時は、読み取った行の処理
   // trueの時は、残り行を読む必要がある。
}
~~~



### 指定の改行コード単位に読み込む

ReadString は、区切り文字が最初に出現するまで読み取り、区切り文字を含む文字列を返す。言い換えると、行末の改行コードは削除されない。

区切り文字を見つける前に エラーが発生した場合、それまに読み取られたデータとエラー自体 (多くの場合 io.EOF) が返される。ReadString は、返されたデータが delim で終わっていない場合にのみ、err != nil を返す。単純な使い方であれば、スキャナーの方が便利かも。

~~~
file, err := os.Open(filename)
reader := bufio.NewReader(file)
for {
   line, err := reader.ReadString(`\n`)
   if err == io.EOF {
      break
   }
   // エラー処理と行の処理
}
~~~



### メモリ単位、または、一括で書き込む

処理したメモリ上のデータを一回で書き込む。
Marshalerによって、メモリ上の構造体から、XML,JSON,YAMLなどに変換する。その後、変換データはバイト型で出力されるので、Write(buf)で一度に書き込むことができる。

~~~
file, err := os.Create(filename)
_, err := file.Write(buf)
~~~


### 文字列や行の単位に書き込む
文字列単位で、ファイルに書き出すことができる。次のバッファ単位で書き出す方が便利かもしれない。しかし、ログ出力の様に、プロセスが突然停止することによるデータの損失のリスクを軽減したいケースに利用できる。

~~~
file, err := os.Create(filename)
_, err := file.WriteString(line)
~~~


### バッファ単位に書き込む
バッファ単位に書き込むため、処理速度が向上する。そして、フォーマット付きプリント文を利用できる点も大きい。YAML,JSON,XMLなどのテキスト出力に適する。

~~~
file, err := os.Create(filename)
writer := bufio.NewWriter(file)

// データの型にあった書き込み関数を選択
_, err := writer.Write(byteBuf)
_, err := writer.WriteString(line)
_, err := fmt.Fprint(writer, lines)

// フォーマット付き出力にも利用できる
_, err := fmt.Fprintf((writer, "%d: %v\n", i line)
_, err := fmt.Fprintln((writer, i, line)

// 最後にバッファをフラッシュする
writer.Flush()
~~~


## まとめ

osパッケージでファイルを開閉と低レベルの読書、bufioパッケージで改行コードで分けられる行単位の処理、ioパッケージはosパッケージを抽象化である。stirngパッケージを併用することもある。
それぞれのパッケージの機能のほんの一部であるが、ここまで抑えておけば、個別に調べて対応できると思う。

## 参考資料
* Goでテキストファイルを読み書きする時に使う標準パッケージ,https://qiita.com/qt-luigi/items/2c13ad68e7d9f8f8c0f2
* Go言語でファイル・IO・ストリームを使った読み書き方法のメモ, https://www.kwbtblog.com/entry/2020/05/07/014836
* os package, https://pkg.go.dev/os
* bufio package, https://pkg.go.dev/bufio
* io package, https://pkg.go.dev/io
* fmt package, https://pkg.go.dev/fmt
* string package, https://pkg.go.dev/strings