Goでテキストファイルを読み書きする時に使う標準パッケージ

先日Goでテキストファイルを読み書きする機会があり、その時に調べた自分用メモです。インターネット上には同様の内容の記事が数多く存在しておりますので、そちらも検索＆参照してみてください。

# 私なりのざっくりイメージ

- バイト配列や文字列の単位で読み書きするなら「[os](https://golang.org/pkg/os/)」パッケージ
- バッファリングしながら読み書きするなら「[bufio](https://golang.org/pkg/bufio/)」パッケージ
- 一括で読み書きするなら「[ioutil](https://golang.org/pkg/io/ioutil/)」パッケージ

# 共通の注意点

- main()関数ではなく別関数にしているコードは、ファイルを閉じる処理のdefer呼び出しを有効にするためです。
- 一括の場合、ファイルの内容を全てメモリに保持するのでメモリ不足などの注意が必要です。
- 各関数やメソッドによって改行コードの出力有無が異なるので、その辺りも楽しんでみてください。
# 読み込み

- 読み込むテキストファイル「read.txt」の内容は次の通りです。
- 各Goコードを実行すると同じ内容が標準出力されます。

```bash:read.txt
$ cat read.txt 
１２３４５
あいうえお
1234567890
```

## バイト配列単位

- osパッケージ

```func (f *File) Read(b []byte) (n int, err error)```

```go
package main

import (
	"fmt"
	"io"
	"os"
)

func main() {
	if err := readBytes("read.txt"); err != nil {
		fmt.Println(os.Stderr, err)
		os.Exit(1)
	}
}

func readBytes(filename string) error {
	file, err := os.Open(filename)
	if err != nil {
		return err
	}
	defer file.Close()

	b := make([]byte, 10)
	for {
		c, err := file.Read(b)
		if c == 0 {
			break
		}
		if err == io.EOF {
			break
		}
		if err != nil {
			return err
		}
		line := string(b[:c])
		fmt.Print(line)
	}
	return nil
}
```
※ 変数 line には改行コードが含まれます。

## 行（改行コード）単位

- bufioパッケージ

```func (s *Scanner) Text() string```

```go
package main

import (
	"bufio"
	"fmt"
	"os"
)

func main() {
	if err := readLine("read.txt"); err != nil {
		fmt.Println(os.Stderr, err)
		os.Exit(1)
	}
}

func readLine(filename string) error {
	file, err := os.Open(filename)
	if err != nil {
		return err
	}
	defer file.Close()

	scanner := bufio.NewScanner(file)
	for scanner.Scan() {
		line := scanner.Text()
		fmt.Println(line)
	}
	if err := scanner.Err(); err != nil {
		return err
	}
	return nil
}
```
※ 変数 line には改行コードは含まれません。

## 行（区切り文字指定）単位

- bufioパッケージ

```func (b *Reader) ReadString(delim byte) (string, error)```

```go
package main

import (
	"bufio"
	"fmt"
	"io"
	"os"
)

func main() {
	if err := readLineDelim("read.txt", '\n'); err != nil {
		fmt.Println(os.Stderr, err)
		os.Exit(1)
	}
}

func readLineDelim(filename string, delim byte) error {
	file, err := os.Open(filename)
	if err != nil {
		return err
	}
	defer file.Close()

	reader := bufio.NewReader(file)
	for {
		line, err := reader.ReadString(delim)
		if err == io.EOF {
			break
		}
		if err != nil {
			return err
		}
		fmt.Print(line)
	}
	return nil
}
```
※ ファイルにキャリッジリターンが含まれる場合は変数  line にもキャリッジリターンが含まれます。

## 行単位

- bufioパッケージ

```func (b *Reader) ReadLine() (line []byte, isPrefix bool, err error)```

```go
package main

import (
	"bufio"
	"fmt"
	"io"
	"os"
)

func main() {
	if err := readLine("read.txt"); err != nil {
		fmt.Println(os.Stderr, err)
		os.Exit(1)
	}
}

func readLine(filename string) error {
	file, err := os.Open(filename)
	if err != nil {
		return err
	}
	defer file.Close()

	reader := bufio.NewReader(file)

	for {
		line, isPrefix, err := reader.ReadLine()
		if err == io.EOF {
			break
		}
		if err != nil {
			return err
		}
		fmt.Print(string(line))
		if !isPrefix {
			fmt.Println()
		}
	}
	return nil
}
```
※ 変数 line に改行コードは含まれません。ただし isPrefix が true の場合は長い行の途中を読み込んでいる事を示します。

## 一括

- ioutilパッケージ

```func ReadFile(filename string) ([]byte, error)```

```go
package main

import (
	"fmt"
	"os"
	"io/ioutil"
)

func main() {
	b, err := ioutil.ReadFile("read.txt")
	if err != nil {
		fmt.Println(os.Stderr, err)
		os.Exit(1)
	}
	lines := string(b)
	fmt.Print(lines)
}
```

# 書き込み

- 書き出すテキストファイル「write.txt」の内容は次の通りです。
- 各Goコードを実行すると同階層にファイル出力されます。

```bash:write.txt
$ cat write.txt 
１２３４５
あいうえお
1234567890
```

## バイト配列単位

- osパッケージ

```func (f *File) Write(b []byte) (n int, err error)```

```go
package main

import (
	"fmt"
	"os"
)

var (
	lines = []string{"１２３４５\n", "あいうえお\n", "1234567890\n"}
)

func main() {
	if err := writeByres("write.txt"); err != nil {
		fmt.Println(os.Stderr, err)
		os.Exit(1)
	}
}

func writeByres(filename string) error {
	file, err := os.Create(filename)
	if err != nil {
		return err
	}
	defer file.Close()

	for _, line := range lines {
		b := []byte(line)
		_, err := file.Write(b)
		if err != nil {
			return err
		}
	}
	return nil
}
```

## 行（文字列）単位

- osパッケージ

```func (f *File) WriteString(s string) (n int, err error)```

- fmtパッケージ

```func Fprint(w io.Writer, a ...interface{}) (n int, err error)```

```go
package main

import (
	"fmt"
	"os"
)

var (
	lines = []string{"１２３４５\n", "あいうえお\n", "1234567890\n"}
)

func main() {
	if err := writeLine("write.txt"); err != nil {
		fmt.Println(os.Stderr, err)
		os.Exit(1)
	}
}

func writeLine(filename string) error {
	file, err := os.Create(filename)
	if err != nil {
		return err
	}
	defer file.Close()

	for _, line := range lines {
		_, err := file.WriteString(line)
		// fmt.Fprint()の場合
		// _, err := fmt.Fprint(file, line)
		if err != nil {
			return err
		}
	}
	return nil
}
```

## バッファリング

- bufioパッケージ

```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 ...interface{}) (n int, err error)```

```go
package main

import (
	"bufio"
	"fmt"
	"os"
)

var (
	lines = []string{"１２３４５\n", "あいうえお\n", "1234567890\n"}
)

func main() {
	if err := writeBuffering("write.txt"); err != nil {
		fmt.Println(os.Stderr, err)
		os.Exit(1)
	}
}

func writeBuffering(filename string) error {
	file, err := os.Create(filename)
	if err != nil {
		return err
	}
	defer file.Close()

	writer := bufio.NewWriter(file)
	if _, err := writer.Write([]byte(lines[0])); err != nil {
		return err
	}
	if _, err := writer.WriteString(lines[1]); err != nil {
		return err
	}
	if _, err := fmt.Fprint(writer, lines[2]); err != nil {
		return err
	}
	writer.Flush()

	return nil
}
```

## 一括

- ioutilパッケージ

```func WriteFile(filename string, data []byte, perm os.FileMode) error```

```go
package main

import (
	"fmt"
	"io/ioutil"
	"os"
)

var (
	lines = []string{"１２３４５\n", "あいうえお\n", "1234567890\n"}
)

func main() {
	b := []byte{}
	for _, line := range lines {
		ll := []byte(line)
		for _, l := range ll {
			b = append(b, l)
		}
	}

	err := ioutil.WriteFile("write.txt", b, 0666)
	if err != nil {
		fmt.Println(os.Stderr, err)
		os.Exit(1)
	}
}
```

# 成果物

普段は開発モードで管理しているソースコードに対してテスト環境や本番環境にデプロイするために差分となる設定値を書き換えるための「ファイル内文字列置換ツール」を作成しました。対象ファイルのサイズが小さいため今回は一括（[ioutil](https://golang.org/pkg/io/ioutil/)パッケージ）を選択しました。

[GitHub - qt-luigi/replacer: replacer replace specified old string to new string in a file. A file is overwritten.](https://github.com/qt-luigi/replacer)