Closed7

Goのファイル操作(I/O)についてまとめる

ハガユウキハガユウキ

[到達したい状態]

  • Goのファイル操作をサクッとできるようにする。

[やること]

  • Goのファイル操作のパッケージを調べる。
ハガユウキハガユウキ

os.File

  • os.FIle型は、ファイルやディレクトリなどのファイルシステムを抽象化した構造体型です。様々なメソッドが用意されています。
  • os.FIle型のファイルへの入出力には[]byte型を利用します。
    • 自分が持っているLinuxの本では、ストリームからバイト列を取り出すことを「読む(read)」といい、ストリームにバイト列を流し込むことを「書く(Write)」と呼んでいる。なので、os.FIle型のファイルへの入出力には[]byte型を利用するっていうのはなんとなく理解できる。

ファイルを読み込み専用でオープンして、ファイルの内容を出力する - *os.File.Read

os.Openを利用すると、引数で指定したファイルを読み込み専用でオープンします。
その後、ファイルの内容を出力したい場合、*os.File.Readを使用します。*os.FIle.Readは、Fileからleb(data)バイトまで読み込んで、引数に指定したdataに格納する。

func main() {
	f, _ := os.Open("foo.txt")
	// byteが1024個あることを意味する
	data := make([]byte, 1024)
	// Fileからleb(data)バイトまで読み込んで、dataに格納する
	// 読み込んだバイト数(バイトの個数ってこと)を返す。エラーがあればエラーも返す
	n, err := f.Read(data)
	if err != nil {
		logger := GetLogger()
		logger.Warn("Hello zap", zap.String("key", "value"), zap.Time("now", time.Now()))
	}
	defer f.Close()

	fmt.Println(n) // => 11
	// 必要なバイト数だけdataから取得する
	fmt.Println(string(data[:n])) // => Hello World
}

ファイルへの書き込み - *os.File.Write

*os.File.Writeを利用すると、引数に指定したバイトスライスをファイルに書き込むことができます。
ファイルのフラグが読み込みしかない場合、書き込みに失敗します。そのため、ファイルをオープンする際に書き込み権限のフラグも付与しましょう。

ファイルに読み込み権限のフラグしか付与しない場合

func main() {
	f, _ := os.Open("foo.txt")
	defer f.Close()

	data := []byte("write file")

	// このコードの場合、ファイルに書き込み権限が付与されていないので、書き込みができなくてエラーが起きる
	_, err := f.Write(data)
	if err != nil {
		logger := GetLogger()
		logger.Warn("Hello zap", zap.Error(err), zap.Time("now", time.Now()))
		// => write foo.txt: bad file descriptor
	}

	o := make([]byte, 1024)
	c, err := f.Read(o)
	if err != nil {
		logger := GetLogger()
		logger.Warn("Hello zap", zap.Error(err), zap.Time("now", time.Now()))
	}

	fmt.Println(string(data[:c])) // => Hello World
}

ファイルに読み書き権限のフラグを指定した場合

os.O_APPENDはファイルの末尾から書き込むことができる。このフラグを指定しないと、ファイルの先頭から書き込もうとするから結果的に上書きされちゃう。また、os.O_APPENDフラグを指定すると、ファイルの末尾から読み取り操作をしようとするので注意。読み取れるデータがなくてEOFエラーが出てしまう。EOFエラーを回避するには、ファイルの読み取り位置をファイルの先頭に戻す必要がある。これはf.Seekを使って読み取り位置を変更すればOK。ちなみに、オフセットとは、位置を基準点からの距離で表した値のこと

func main() {
	f, _ := os.OpenFile("foo.txt", os.O_RDWR|os.O_APPEND, 0644)
	defer f.Close()

	data := []byte("write file\n")

	_, err := f.Write(data)
	if err != nil {
		logger := GetLogger()
		logger.Warn("Hello zap", zap.Error(err), zap.Time("now", time.Now()))
	}

	// ファイルの先頭から0バイト目を読み込む
	// 第二引数の値によって基準が決まる
	// オフセットとは、位置を基準点からの距離で表した値のこと
	f.Seek(0, io.SeekStart)

	o := make([]byte, 80000)
	c, err := f.Read(o)
	if err != nil {
		logger := GetLogger()
		logger.Warn("Hello zap", zap.Error(err), zap.Time("now", time.Now()))
	}

	fmt.Println(string(o[:c])) // => Hello World
}

https://qiita.com/pseuxide/items/fae07e8533b36f553714
https://zenn.dev/hsaki/books/golang-io-package/viewer/file
https://reiki4040.hatenablog.com/entry/2018/08/13/080000
https://pkg.go.dev/os#File.Seek
https://wa3.i-3-i.info/word11923.html

ハガユウキハガユウキ

ちなみに、OpenFileにos.O_CREATEを指定した場合、ファイルが存在しなければファイルを新規作成してくれる。そもそもファイルを新規作成したいなら、os.Createを使った方が良いけど。

	f, _ := os.OpenFile("ope.txt", os.O_RDWR|os.O_APPEND|os.O_CREATE, 0644)
ハガユウキハガユウキ

指定したファイル名のファイルの内容をバイトスライスで取得する - os.ReadFIle

os.ReadFileを使用すると、指定したファイルの内容をバイトスライスで取得できます。取得したバイトスライスはstring型にキャストすることで、文字列として出力できます。
*os.File.Readではなくos.ReadFIleを使用するメリット、デメリットとしては、以下が考えられます。

メリット

  • ファイルの内容を出力するためにやっていた一連の処理(ファイルをオープンして、自分で定義したバイトスライスにファイルの内容を書き込む)を書かなくて済む。
  • ファイルをクローズする処理を書かなくて済む。

デメリット

  • ファイルオブジェクトを生成するわけではないので、ファイルに書き込んだ後にファイルの内容を出力するなどの柔軟な処理はできない。
func main() {
	b, err := os.ReadFile("foo.txt")
	if err != nil {
		logger := GetLogger()
		logger.Warn("Hello zap", zap.Error(err), zap.Time("now", time.Now()))
	}
	fmt.Println(string(b)) // => Hello World
}

開いているファイル(ファイルオブジェクト)の内容を一度に読み込んでバイトスライスを取得する - io.ReadAll

io.ReadAllを利用することで、開いているファイル(ファイルオブジェクト)の内容を一度に読み込んで、バイトスライスを取得できます。取得したバイトスライスはstring型にキャストすることで、文字列として出力できます。
*os.File.Readではなくio.ReadAllを使用するメリット、デメリットとしては、以下が考えられます。

メリット

  • ファイルを出力する際の一連の処理(自分で定義したバイトスライスにファイルの内容を書き込む)を書く必要がなくなる。
    • バイトスライスを定義する際に適切なサイズを自分で決める必要があったが、その必要もなくなる。
  • ファイルオブジェクトを事前に生成する必要があるので、書き込みをした後にファイルの内容を出力するなど、柔軟性がある。

デメリット

  • ファイルの内容を見たいのに、ファイルのオープン処理とクローズ処理を書く必要がある。
    • ただファイルの内容を見たいだけなら、ReadFileの方が優れている。しかし、ReadFIleにはファイルを書き込んだ後にファイルの内容を見るような柔軟性がない。
    • ReadFIleを使って、更新されたファイルを見ることができた。ReadFileの場合、書き込んだ後にSeek処理を書く必要がないから楽である。しかし、ファイル名を何回も指定するので、どのファイルを操作しているのか分かりづらい。仮に変数でファイル名を保持していたとしても、変数を見るために上下にスクロールを動かさないといけないので、めんどくさい。
func main() {
	f, err := os.Open("ope.txt")
	if err != nil {
		logger := GetLogger()
		logger.Warn("Hello zap", zap.Error(err), zap.Time("now", time.Now()))
	}
	defer f.Close()

	b, err := io.ReadAll(f)
	if err != nil {
		logger := GetLogger()
		logger.Warn("Hello zap", zap.Error(err), zap.Time("now", time.Now()))
	}

	fmt.Println(string(b)) // => Hello World
}

https://pkg.go.dev/io

ハガユウキハガユウキ

指定したファイル名のファイルに、バイトスライスを書き込む - os.WriteFile

os.WriteFileを利用することで、指定したファイル名のファイルにバイトスライスを書き込むことができる。 ファイルが存在しない場合、ファイルを作成する。 ファイルがすでに存在している場合、ファイルの内容を上書きしてしまうので注意が必要である。個人的にはファイルに追加で何か書きたいなら、このos.WriteFileではなくて、OpenFIleとWriteの組み合わせの方が良いと思われる。

メリット

  • ファイルを作成して書き込むという一連の流れを、このメソッドを呼び出すだけで実行できる。
    • 既存のファイルに書き込む用途でWriteFile使うのは、めんどくさいのであまりお勧めできない。
    • ファイルを作成して書き込みたい場合、WriteFileはオススメ
  • ファイルのオープン、クローズ処理を書かなくて済む。

デメリット

  • ファイルがすでに存在している場合、ファイルの内容を上書きしてしまう。上書きしないようにするためには、まず、ファイルからファイルオブジェクトを作成した後に、ファイルの内容のバイトスライスを取得する。その後、バイトスライスと新しく書き込みたい内容を結合してWriteFileに書き込めば良い。しかしこれはめんどくさい。これをするなら、末尾フラグを指定したOpenFileを実行した後に、Writeで書き込んだ方が良い。
func main() {
	err := os.WriteFile("opeii.txt", []byte("heeloo\n"), 0666)
	if err != nil {
		logger := GetLogger()
		logger.Warn("Hello zap", zap.Error(err), zap.Time("now", time.Now()))
	}

	b, err := os.ReadFile("opeii.txt")
	if err != nil {
		logger := GetLogger()
		logger.Warn("Hello zap", zap.Error(err), zap.Time("now", time.Now()))
	}

	fmt.Println(string(b)) // => Hello World
}

https://maku77.github.io/p/6eimpsv/#section-3

ハガユウキハガユウキ

ファイルなどの内容を一行ずつ処理する - bufio.NewScanner

ファイルを改行区切りで一行ずつ処理したい場合、 bufio.NewScannerを使用する。
NewScannerの詳しい説明については、以下のスクラップに書いてある。

NewScannerはrから読み込む新しいScannerを返します。分割関数のデフォルトはScanLinesです。

https://zenn.dev/yukihaga/scraps/0508d3fc864b57

func main() {
	f, err := os.Open("foo.txt")
	if err != nil {
		logger := GetLogger()
		logger.Warn("Hello zap", zap.Error(err), zap.Time("now", time.Now()))
	}

	// 引数の値から、スキャナーを作成
	scanner := bufio.NewScanner(f)

	// Scanメソッドを呼び出したときに、スキャン処理が実行される。デフォルトでは一行スキャンされる。スキャン処理が成功する限りはtrueを返し続ける。
	// スキャンした行はTextメソッドで取得できる
	// NewScannerに標準入力を指定している場合、バッファに標準入力からバイト列が渡されるまではスキャンを待機している。
	for i := 1; scanner.Scan(); i++ {
		line := scanner.Text()
		fmt.Println(line)
	}
}

https://golang.hateblo.jp/entry/2018/11/09/163000#ファイルなどの内容を一行ずつ処理する---Scanner

ハガユウキハガユウキ

まとめ

読み込み系のメソッド

  • *os.File.Read
  • os.ReadFIle

書き込み系のメソッド

  • *os.File.Write
  • os.WriteFile

一行ごとに処理するメソッド

  • bufio.NewScanner
このスクラップは2023/06/21にクローズされました