[Goのはじめ方] Part23: ファイル操作(os, io/ioutil, bufio)

Go

Go言語でのプログラミングにおいて、ファイルの読み書きは非常に一般的なタスクです。設定ファイルの読み込み、ログの出力、データの永続化など、様々な場面でファイル操作が必要になります。 このセクションでは、Goでファイル操作を行うための主要なパッケージである os, io/ioutil (非推奨), bufio の基本的な使い方を学びます。💡

それぞれのパッケージが持つ機能と特徴を理解し、状況に応じて適切なツールを選択できるようになりましょう!

1. os パッケージ: 基本的なファイル操作

os パッケージは、オペレーティングシステムが提供する機能にアクセスするためのインターフェースを提供します。ファイル操作に関する基本的な機能の多くがこのパッケージに含まれています。

ファイルの作成、オープン、クローズ

ファイルの読み書きを行う最初のステップは、ファイルを開く(または作成する)ことです。

  • ファイル作成 (新規作成・上書き): os.Create(name string) (*File, error)
  • ファイルオープン (読み込み専用): os.Open(name string) (*File, error)
  • ファイルオープン (詳細設定): os.OpenFile(name string, flag int, perm FileMode) (*File, error)
    • flag: ファイルアクセスモード (例: os.O_RDONLY, os.O_WRONLY, os.O_RDWR, os.O_CREATE, os.O_APPEND)。これらは | (ビットOR) で組み合わせ可能です。
    • perm: ファイル作成時のパーミッション (例: 0666)。
  • クローズ: file.Close() error重要: 開いたファイルは必ず Close() する必要があります。defer 文を使うのが一般的です。

コード例:

package main

import (
	"fmt"
	"os"
)

func main() {
	// 新規ファイル作成 (既に存在する場合は上書き)
	file, err := os.Create("example.txt")
	if err != nil {
		fmt.Println("Error creating file:", err)
		return
	}
	// deferで確実にファイルをクローズする
	defer file.Close()

	fmt.Println("File created successfully:", file.Name())

	// 読み込み用にファイルを開く
	readFile, err := os.Open("example.txt")
	if err != nil {
		fmt.Println("Error opening file:", err)
		return
	}
	defer readFile.Close()

	fmt.Println("File opened successfully for reading:", readFile.Name())

	// 書き込み用に追記モードでファイルを開く (なければ作成)
	appendFile, err := os.OpenFile("log.txt", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
	if err != nil {
		fmt.Println("Error opening file for append:", err)
		return
	}
	defer appendFile.Close()
	fmt.Println("File opened successfully for appending:", appendFile.Name())
}

ファイルへの書き込み ✍️

開いたファイルに対してデータを書き込むには、Write メソッドや WriteString メソッドなどを使用します。

  • file.Write(b []byte) (n int, err error): バイトスライスを書き込みます。
  • file.WriteString(s string) (n int, err error): 文字列を書き込みます。
  • file.WriteAt(b []byte, off int64) (n int, err error): 指定したオフセット位置からバイトスライスを書き込みます。

コード例:

package main

import (
	"fmt"
	"os"
)

func main() {
	file, err := os.Create("output.txt")
	if err != nil {
		fmt.Println("Error creating file:", err)
		return
	}
	defer file.Close()

	// バイトスライスを書き込む
	data := []byte("Hello, Go!\n")
	n1, err := file.Write(data)
	if err != nil {
		fmt.Println("Error writing bytes:", err)
		return
	}
	fmt.Printf("Wrote %d bytes\n", n1)

	// 文字列を書き込む
	n2, err := file.WriteString("ファイル書き込みのテストです。\n")
	if err != nil {
		fmt.Println("Error writing string:", err)
		return
	}
	fmt.Printf("Wrote %d bytes\n", n2)

	fmt.Println("Successfully wrote to output.txt")
}

ファイルからの読み込み 📖

ファイルからデータを読み込むには、Read メソッドを使用します。一度に読み込むサイズを指定するためのバイトスライス(バッファ)を用意します。

  • file.Read(b []byte) (n int, err error): ファイルからデータを読み込み、指定されたバイトスライス b に格納します。読み込んだバイト数とエラーを返します。ファイルの終端に達すると io.EOF エラーを返します。
  • file.ReadAt(b []byte, off int64) (n int, err error): 指定したオフセット位置からデータを読み込みます。

コード例 (少しずつ読み込む):

package main

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

func main() {
	file, err := os.Open("output.txt") // 上で作成したファイルを読み込む
	if err != nil {
		fmt.Println("Error opening file:", err)
		return
	}
	defer file.Close()

	buffer := make([]byte, 64) // 64バイトのバッファを作成
	for {
		n, err := file.Read(buffer)
		if err == io.EOF {
			// ファイルの終端に達したらループを抜ける
			break
		}
		if err != nil {
			fmt.Println("Error reading file:", err)
			return
		}
		// 読み込んだデータを出力 (読み込んだバイト数nだけ)
		fmt.Print(string(buffer[:n]))
	}
	fmt.Println("\nFinished reading file.")
}

ファイル/ディレクトリ情報の取得

ファイルサイズ、更新日時、パーミッションなどのメタ情報を取得するには os.Stat または file.Stat を使用します。

  • os.Stat(name string) (FileInfo, error): パス名を指定してファイル情報を取得します。
  • file.Stat() (FileInfo, error): 開いているファイルの情報を取得します。

返される FileInfo インターフェースから様々な情報を取得できます。

  • Name() string: ファイル名
  • Size() int64: ファイルサイズ (バイト単位)
  • Mode() FileMode: ファイルモード (パーミッション、ディレクトリか否かなど)
  • ModTime() time.Time: 最終更新日時
  • IsDir() bool: ディレクトリかどうか

コード例:

package main

import (
	"fmt"
	"os"
	"time"
)

func main() {
	fileInfo, err := os.Stat("output.txt")
	if err != nil {
		if os.IsNotExist(err) {
			fmt.Println("File does not exist.")
		} else {
			fmt.Println("Error getting file info:", err)
		}
		return
	}

	fmt.Println("File Name:", fileInfo.Name())
	fmt.Println("Size:", fileInfo.Size(), "bytes")
	fmt.Println("Permissions:", fileInfo.Mode())
	fmt.Println("Last Modified:", fileInfo.ModTime().Format(time.RFC3339))
	fmt.Println("Is Directory:", fileInfo.IsDir())
}

ディレクトリ操作

ディレクトリの作成や削除も os パッケージで行えます。

  • os.Mkdir(name string, perm FileMode) error: ディレクトリを1階層作成します。
  • os.MkdirAll(path string, perm FileMode) error: 必要な親ディレクトリも含めてパス全体を作成します。
  • os.Remove(name string) error: ファイルまたは空のディレクトリを削除します。
  • os.RemoveAll(path string) error: 指定したパスとその中身(ファイルやサブディレクトリ)をすべて削除します。⚠️注意して使用してください!
  • os.ReadDir(name string) ([]DirEntry, error): ディレクトリ内のエントリ一覧 (ファイルやサブディレクトリ) を取得します。 (Go 1.16以降推奨)

コード例:

package main

import (
	"fmt"
	"os"
)

func main() {
	// ディレクトリ作成
	err := os.Mkdir("my_dir", 0755)
	if err != nil && !os.IsExist(err) { // 既に存在する場合のエラーは無視
		fmt.Println("Error creating directory:", err)
		return
	}
	fmt.Println("Directory 'my_dir' created or already exists.")

	// ネストしたディレクトリ作成
	err = os.MkdirAll("parent_dir/child_dir", 0755)
	if err != nil {
		fmt.Println("Error creating nested directories:", err)
		return
	}
	fmt.Println("Nested directories created.")

	// ディレクトリの内容一覧表示
	fmt.Println("\nContents of '.':")
	entries, err := os.ReadDir(".") // カレントディレクトリ
	if err != nil {
		fmt.Println("Error reading directory:", err)
		return
	}
	for _, entry := range entries {
		fmt.Printf("  Name: %s, IsDir: %v\n", entry.Name(), entry.IsDir())
	}

	// ディレクトリ削除 (空の場合)
	err = os.Remove("my_dir")
	if err != nil {
		fmt.Println("\nError removing 'my_dir':", err)
	} else {
		fmt.Println("\nDirectory 'my_dir' removed.")
	}

	// ディレクトリとその中身を削除
	err = os.RemoveAll("parent_dir")
	if err != nil {
		fmt.Println("Error removing 'parent_dir' and its contents:", err)
	} else {
		fmt.Println("Directory 'parent_dir' and its contents removed.")
	}
}

2. io/ioutil パッケージ (非推奨 ⚠️)

注意: io/ioutil パッケージの多くの関数は Go 1.16 以降 非推奨 となり、機能は io パッケージや os パッケージに移動されました。新しいコードでは、可能な限り代替関数を使用することが推奨されます。

かつて io/ioutil パッケージは、ファイル全体を一度に読み書きするなど、便利なユーティリティ関数を提供していました。ここでは、非推奨であることとその代替関数を紹介します。

io/ioutil 関数 (非推奨)代替関数 (推奨)説明
ioutil.ReadFile(filename string) ([]byte, error)os.ReadFile(filename string) ([]byte, error)ファイル全体を読み込み、内容をバイトスライスとして返します。大きなファイルには注意が必要です。
ioutil.WriteFile(filename string, data []byte, perm os.FileMode) erroros.WriteFile(filename string, data []byte, perm os.FileMode) error指定されたバイトスライスをファイルに書き込みます。ファイルが存在しない場合は作成し、存在する場合は上書きします。
ioutil.ReadDir(dirname string) ([]os.FileInfo, error)os.ReadDir(dirname string) ([]os.DirEntry, error)ディレクトリの内容 (ファイルやサブディレクトリの情報) を取得します。os.ReadDir はより効率的な os.DirEntry スライスを返します。
ioutil.TempFile(dir, pattern string) (f *os.File, err error)os.CreateTemp(dir, pattern string) (*os.File, error)一時ファイルを作成します。dir が空文字列ならデフォルトの一時ディレクトリを使用します。pattern はファイル名のプレフィックスとサフィックスを指定します (例: "myapp-*.tmp")。
ioutil.TempDir(dir, pattern string) (name string, err error)os.MkdirTemp(dir, pattern string) (string, error)一時ディレクトリを作成します。

コード例 (代替関数を使用):

package main

import (
	"fmt"
	"os"
)

func main() {
	// ファイル全体を読み込む (推奨: os.ReadFile)
	content, err := os.ReadFile("output.txt") // osパッケージの関数を使う
	if err != nil {
		fmt.Println("Error reading file:", err)
		return
	}
	fmt.Println("File content:\n", string(content))

	// ファイルに一括書き込み (推奨: os.WriteFile)
	newData := []byte("This data will overwrite the file.\nUsing os.WriteFile.")
	err = os.WriteFile("new_output.txt", newData, 0644) // osパッケージの関数を使う
	if err != nil {
		fmt.Println("Error writing file:", err)
		return
	}
	fmt.Println("Successfully wrote to new_output.txt using os.WriteFile")

	// 一時ファイル作成 (推奨: os.CreateTemp)
	tempFile, err := os.CreateTemp("", "my-temp-*.data") // デフォルトの一時ディレクトリに作成
	if err != nil {
		fmt.Println("Error creating temp file:", err)
		return
	}
	fmt.Println("Created temp file:", tempFile.Name())
	// 一時ファイルは不要になったら削除するのが一般的
	defer os.Remove(tempFile.Name()) // deferで後処理
	defer tempFile.Close()

	_, err = tempFile.WriteString("Temporary data")
	if err != nil {
		fmt.Println("Error writing to temp file:", err)
	}
}

古いコードで io/ioutil を見かけた場合は、これらの代替関数への置き換えを検討しましょう。

3. bufio パッケージ: バッファ付きI/O 🚀

os パッケージの ReadWrite は直接システムコールを呼び出すため、小さなデータを頻繁に読み書きすると効率が悪くなることがあります。 bufio パッケージは、内部にバッファ(メモリ領域)を持つことで、I/O操作をまとめて行い、パフォーマンスを向上させます。特にテキストファイルを一行ずつ読み込む場合などに便利です。

バッファ付きリーダー (bufio.Reader)

bufio.NewReader(rd io.Reader) を使って、既存の io.Reader (例えば os.File) からバッファ付きリーダーを作成します。

  • reader.Read(p []byte) (n int, err error): バッファから読み込みます。バッファが空なら元のReaderから読み込みます。
  • reader.ReadByte() (byte, error): 1バイト読み込みます。
  • reader.ReadRune() (r rune, size int, err error): 1つのUTF-8エンコードされたルーンを読み込みます。
  • reader.ReadLine() (line []byte, isPrefix bool, err error): 1行読み込みますが、行がバッファより長い場合や改行コードを含まないなど、低レベルな操作です。通常は ReadStringScanner を使います。
  • reader.ReadString(delim byte) (string, error): 指定された区切り文字 (delim) まで読み込み、文字列として返します。行単位で読み込む場合は '\n' を指定します。
  • reader.Peek(n int) ([]byte, error): 次の n バイトを読み込まずに覗き見します。

コード例 (一行ずつ読み込む):

package main

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

func main() {
	// 書き込み例のため、サンプルファイルを作成
	content := `First line.
Second line with some text.
Third line, the last one.`
	err := os.WriteFile("lines.txt", []byte(content), 0644)
	if err != nil {
		fmt.Println("Error creating sample file:", err)
		return
	}


	file, err := os.Open("lines.txt")
	if err != nil {
		fmt.Println("Error opening file:", err)
		return
	}
	defer file.Close()

	reader := bufio.NewReader(file)
	fmt.Println("Reading lines using bufio.Reader:")
	lineNum := 1
	for {
		line, err := reader.ReadString('\n') // 改行コードまで読み込む
		// 読み込んだ行から改行コードを取り除く (OSによって改行コードが異なる場合も考慮)
		line = strings.TrimRight(line, "\r\n")

		if len(line) > 0 { // 空行でなければ出力
			fmt.Printf("Line %d: %s\n", lineNum, line)
			lineNum++
		}

		if err == io.EOF {
			// ファイルの終端に達したら終了
			break
		}
		if err != nil {
			fmt.Println("Error reading file:", err)
			return
		}
	}
	fmt.Println("Finished reading with bufio.Reader.")
}

バッファ付きライター (bufio.Writer)

bufio.NewWriter(wr io.Writer) を使って、既存の io.Writer からバッファ付きライターを作成します。書き込み操作はまずバッファに対して行われ、バッファがいっぱいになるか、明示的に Flush() を呼び出したときに、実際の書き込みが行われます。

  • writer.Write(p []byte) (nn int, err error): バイトスライスをバッファに書き込みます。
  • writer.WriteString(s string) (int, error): 文字列をバッファに書き込みます。
  • writer.WriteByte(c byte) error: 1バイト書き込みます。
  • writer.WriteRune(r rune) (size int, err error): 1ルーン書き込みます。
  • writer.Flush() error: 重要: バッファに残っているデータをすべて基礎となる io.Writer に書き込みます。プログラム終了前や、書き込みを確実に反映させたいタイミングで必ず呼び出す必要があります。defer で呼び出すのが一般的です。
  • writer.Available() int: バッファの空きバイト数を返します。
  • writer.Buffered() int: バッファに溜まっているバイト数を返します。

コード例:

package main

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

func main() {
	file, err := os.Create("buffered_output.txt")
	if err != nil {
		fmt.Println("Error creating file:", err)
		return
	}
	defer file.Close() // ファイルのクローズをdefer

	writer := bufio.NewWriter(file)
	// writer.Flush() も defer で呼び出すのが安全
	// FlushはCloseより先に実行される必要があるため、こちらを先にdeferする
	defer writer.Flush()

	fmt.Println("Writing with bufio.Writer...")

	_, err = writer.WriteString("This is the first line written via bufio.Writer.\n")
	if err != nil {
		fmt.Println("Error writing string:", err)
		return
	}
	fmt.Printf("Buffered: %d bytes\n", writer.Buffered()) // バッファに溜まっているデータ量

	_, err = writer.Write([]byte("Writing a byte slice next.\n"))
	if err != nil {
		fmt.Println("Error writing bytes:", err)
		return
	}
	fmt.Printf("Buffered: %d bytes\n", writer.Buffered())

	err = writer.WriteByte('!') // 1バイト書き込み
	if err != nil {
		fmt.Println("Error writing byte:", err)
		return
	}
	fmt.Printf("Buffered: %d bytes\n", writer.Buffered())

	// Flushが呼ばれるまでファイルには書き込まれない可能性がある
	fmt.Println("Calling Flush...")
	// deferでFlush()を呼んでいるため、ここで明示的に呼ぶ必要はないが、
	// 任意のタイミングで書き込みたい場合は Flush() を呼び出す。
	// writer.Flush() // ここで呼ぶと即座に書き込まれる

	fmt.Println("Finished writing (Flush will happen on exit due to defer).")
}

bufio.Writer を使う際は、最後に必ず Flush() を呼び出すことを忘れないでください。 これを忘れると、データの一部または全部がファイルに書き込まれない可能性があります。defer writer.Flush() を使うと忘れにくいです。

4. まとめ

Goでのファイル操作は、主に以下のパッケージを使って行います。

  • os: ファイルのオープン、クローズ、読み書き、情報取得、ディレクトリ操作など、基本的なファイルシステム操作を提供します。シンプルなファイル操作や、os.ReadFile, os.WriteFile による一括処理に適しています。
  • io/ioutil: 非推奨。 便利な関数がありましたが、現在は osio パッケージの関数で代替されます。
  • bufio: バッファリングにより、頻繁な読み書きやテキストファイルの行単位処理のパフォーマンスを向上させます。大きなファイルやストリーム処理に適しています。Writer を使う際は Flush() を忘れないようにしましょう。

これらのパッケージを理解し、適切に使い分けることで、効率的で信頼性の高いファイル操作を行うことができます。🎉

次のステップでは、net/http パッケージを使ったHTTPサーバの構築方法について学びます。Webアプリケーション開発への第一歩です!

コメント

タイトルとURLをコピーしました