ファイルI/O

レッスン20:ファイルI/O

生活での例え

あなたが図書館の司書であると想像してください:

図書館に厳しい貸出規則があるように、オペレーティングシステムにもファイルに対する権限制御があります。ファイルを読み書きするには「図書館カード」(権限)が必要で、使い終わったら速やかに「返却」(閉じる)しなければなりません。さもないと他の読者が利用できなくなります。


コアコンセプト

Goは標準ライブラリを通じて豊富なファイルI/O機能を提供しています。主に以下のパッケージが関わります:

パッケージ 用途
os 低レベルファイル操作:オープン、作成、削除、名前変更
io 汎用I/Oインターフェース:ReaderWriter
bufio バッファリングされた読み書き:ScannerReaderWriter
io/ioutil(非推奨) Go 1.16以降、機能はosパッケージに移動
filepath クロスプラットフォーム対応のパス処理:結合、分割、マッチング

基本的なファイル操作フロー

TEXT
ファイルを開く → 読み/書き操作 → ファイルを閉じる
   │                       │
   └── defer f.Close() ────┘   // 関数終了時に確実に閉じる

3つの読み込み方法の比較

方法 特徴 ユースケース
os.ReadFile ファイル全体を一度にメモリに読み込む 小さいファイル(< 100MB)
bufio.Scanner 行ごとにスキャン、メモリフレンドリー 大きいファイル、行ごとの処理
io.ReadAll すべてを[]byteに読み込む ネットワークレスポンスなどのストリーミングデータ

基本構文と使い方

1. ファイルのオープンと作成

GO
package main

import (
    "fmt"
    "os"
)

func main() {
    // 読み取り専用モードでファイルを開く(ファイルが存在する必要がある)
    f, err := os.Open("data.txt")
    if err != nil {
        fmt.Println("オープン失敗:", err)
        return
    }
    defer f.Close() // 💡 常にdeferでファイルを閉じる

    // 読み書きモードでファイルを開く(ファイルが存在する必要がある)
    f2, err := os.OpenFile("data.txt", os.O_RDWR, 0644)
    if err != nil {
        fmt.Println("オープン失敗:", err)
        return
    }
    defer f2.Close()

    // 新しいファイルを作成(存在する場合は切り詰める)
    f3, err := os.Create("newfile.txt")
    if err != nil {
        fmt.Println("作成失敗:", err)
        return
    }
    defer f3.Close()
}
💡 ヒント: os.Openos.OpenFile(name, os.O_RDONLY, 0)と等価です。読み取り専用で書き込みはできません。

2. OpenFileフラグビット

GO
// よく使うフラグビットの組み合わせ
os.O_RDONLY  // 読み取り専用
os.O_WRONLY  // 書き込み専用
os.O_RDWR    // 読み書き
os.O_APPEND  // 追加モード
os.O_CREATE  // ファイルが存在しない場合に作成
os.O_TRUNC   // オープン時に切り詰める(内容をクリア)

// 追加書き込み
f, err := os.OpenFile("log.txt",
    os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644)
💡 ヒント: 権限0644は、所有者は読み書き可能、その他は読み取り専用を意味します(Unixシステム)。

3. ファイルの読み込み

GO
package main

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

func main() {
    // ===== 方法1:ファイル全体を一度に読み込む =====
    data, err := os.ReadFile("config.txt")
    if err != nil {
        fmt.Println("読み込み失敗:", err)
        return
    }
    fmt.Println(string(data))

    // ===== 方法2:行ごとに読み込む(大きいファイルに推奨) =====
    file, err := os.Open("access.log")
    if err != nil {
        fmt.Println("オープン失敗:", err)
        return
    }
    defer file.Close()

    scanner := bufio.NewScanner(file)
    lineNum := 0
    for scanner.Scan() { // 行ごとにスキャン
        lineNum++
        fmt.Printf("行 %d: %s\n", lineNum, scanner.Text())
    }
    if err := scanner.Err(); err != nil {
        fmt.Println("スキャンエラー:", err)
    }
}
💡 ヒント: bufio.Scannerのデフォルトの最大トークン長は64KBです。特に長い行については、scanner.Buffer()を呼び出してバッファサイズを増やしてください。

4. ファイルへの書き込み

GO
package main

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

func main() {
    // ===== 方法1:一度に書き込む =====
    err := os.WriteFile("output.txt", []byte("Hello, Go!\n"), 0644)
    if err != nil {
        fmt.Println("書き込み失敗:", err)
        return
    }

    // ===== 方法2:bufio.Writerを使用(バッファリング) =====
    f, err := os.Create("buffered.txt")
    if err != nil {
        fmt.Println("作成失敗:", err)
        return
    }
    defer f.Close()

    writer := bufio.NewWriter(f)
    for i := 1; i <= 5; i++ {
        fmt.Fprintf(writer, "行 %d の内容\n", i)
    }
    writer.Flush() // 💡 必ずFlushすること。さもないとデータがバッファに残り、ディスクに書き込まれない

    // ===== 方法3:直接書き込み =====
    f2, _ := os.Create("direct.txt")
    defer f2.Close()
    f2.WriteString("文字列を直接書き込み\n")
    f2.Write([]byte("バイトスライスを直接書き込み\n"))
}
💡 ヒント: bufio.Writerは頻繁な小さな書き込みのパフォーマンスを大幅に向上させます。システムコールの回数を減らすためです。ただし、最後には必ずFlush()を呼び出す必要があります。

5. 削除と名前変更

GO
// ファイルを削除
err := os.Remove("temp.txt")
if err != nil {
    fmt.Println("削除失敗:", err)
}

// ディレクトリとその中身をすべて削除
err = os.RemoveAll("temp_dir/")

// ファイルの名前変更 / 移動
err = os.Rename("old.txt", "new.txt")

6. パス処理(filepathパッケージ)

GO
package main

import (
    "fmt"
    "path/filepath"
)

func main() {
    // パスを結合(クロスプラットフォーム対応)
    p := filepath.Join("data", "logs", "app.log")
    fmt.Println(p) // data\logs\app.log(Windows)または data/logs/app.log(Linux)

    // パスを分割
    dir, file := filepath.Split("/home/user/doc.txt")
    fmt.Println("ディレクトリ:", dir)   // /home/user/
    fmt.Println("ファイル:", file)       // doc.txt

    // 拡張子を取得
    ext := filepath.Ext("report.pdf")
    fmt.Println(ext) // .pdf

    // 拡張子なしのファイル名を取得
    name := filepath.Base("report.pdf")
    fmt.Println(name) // report.pdf

    // 絶対パス
    abs, _ := filepath.Abs("relative/path")
    fmt.Println(abs)

    // パスマッチング(globパターン)
    matched, _ := filepath.Match("*.go", "main.go")
    fmt.Println(matched) // true
}
💡 ヒント: パスの結合には常にfilepath.Joinを使用してください。/\\を手動で結合すると、クロスプラットフォームで問題が発生します。

7. ディレクトリ操作と走査

GO
package main

import (
    "fmt"
    "os"
    "path/filepath"
)

func main() {
    // ディレクトリを作成
    os.Mkdir("logs", 0755)
    os.MkdirAll("logs/2024/01", 0755) // 再帰的に作成

    // ディレクトリの内容を読み取る
    entries, err := os.ReadDir(".")
    if err != nil {
        fmt.Println("ディレクトリの読み取りに失敗しました:", err)
        return
    }
    for _, entry := range entries {
        info, _ := entry.Info()
        fmt.Printf("%-10s %8dバイト  %s\n",
            entry.Name(), info.Size(), info.Mode())
    }

    // ディレクトリツリーを再帰的に走査
    fmt.Println("\n=== 再帰的走査 ===")
    filepath.Walk(".", func(path string, info os.FileInfo, err error) error {
        if err != nil {
            return err
        }
        prefix := "📄"
        if info.IsDir() {
            prefix = "📁"
        }
        fmt.Printf("%s %s\n", prefix, path)
        return nil
    })
}
💡 ヒント: filepath.Walkはすべてのファイルを訪問します。非常に大きなディレクトリツリーについては、filepath.WalkDir(Go 1.16+)を使用するとパフォーマンスが向上します。statシステムコールが削減されるためです。


例題

例:ファイルコピーツール(難易度⭐)

任意のサイズのファイルをサポートするシンプルなファイルコピー関数を実装します。

GO
package main

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

// copyFileはソースファイルを宛先パスにコピーします
func copyFile(src, dst string) (int64, error) {
    // ソースファイルを開く
    sourceFile, err := os.Open(src)
    if err != nil {
        return 0, fmt.Errorf("ソースファイルを開けませんでした: %w", err)
    }
    defer sourceFile.Close()

    // ソースファイルの情報を取得(権限設定用)
    sourceInfo, err := sourceFile.Stat()
    if err != nil {
        return 0, fmt.Errorf("ファイル情報を取得できませんでした: %w", err)
    }

    // 宛先ファイルを作成(ソースファイルの権限を継承)
    destFile, err := os.OpenFile(dst,
        os.O_WRONLY|os.O_CREATE|os.O_TRUNC,
        sourceInfo.Mode())
    if err != nil {
        return 0, fmt.Errorf("宛先ファイルを作成できませんでした: %w", err)
    }
    defer destFile.Close()

    // io.Copyでストリーミングコピー(バッファを自動管理)
    bytesWritten, err := io.Copy(destFile, sourceFile)
    if err != nil {
        return 0, fmt.Errorf("データのコピーに失敗しました: %w", err)
    }

    return bytesWritten, nil
}

func main() {
    // テストファイルを作成
    os.WriteFile("source.txt", []byte("これはソースファイルの内容です。\nコピー関数のテストに使用します。\n"), 0644)

    // コピーを実行
    n, err := copyFile("source.txt", "copy.txt")
    if err != nil {
        fmt.Println("エラー:", err)
        return
    }
    fmt.Printf("コピー完了、合計 %d バイト\n", n)

    // 結果を確認
    data, _ := os.ReadFile("copy.txt")
    fmt.Println("コピーされた内容:", string(data))

    // クリーンアップ
    os.Remove("source.txt")
    os.Remove("copy.txt")
}
▶ 試してみよう
TEXT
コピー完了、合計 45 バイト
コピーされた内容: これはソースファイルの内容です。
コピー関数のテストに使用します。

例:ログファイルアナライザー(難易度⭐⭐)

ログファイルを読み取り、レベルごとに集計してサマリーを出力します。

GO
package main

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

// LogStatsはログの統計情報を保持します
type LogStats struct {
    Total   int
    Error   int
    Warning int
    Info    int
    Debug   int
}

// analyzeLogはログファイルを分析し、統計を返します
func analyzeLog(filename string) (*LogStats, error) {
    file, err := os.Open(filename)
    if err != nil {
        return nil, fmt.Errorf("ログファイルを開けませんでした: %w", err)
    }
    defer file.Close()

    stats := &LogStats{}
    scanner := bufio.NewScanner(file)

    // 特に長い行のためにバッファサイズを増やす
    scanner.Buffer(make([]byte, 1024*1024), 1024*1024)

    for scanner.Scan() {
        line := scanner.Text()
        stats.Total++

        switch {
        case strings.Contains(line, "[ERROR]"):
            stats.Error++
        case strings.Contains(line, "[WARNING]"):
            stats.Warning++
        case strings.Contains(line, "[INFO]"):
            stats.Info++
        case strings.Contains(line, "[DEBUG]"):
            stats.Debug++
        }
    }

    if err := scanner.Err(); err != nil {
        return nil, fmt.Errorf("ログの読み取りエラー: %w", err)
    }

    return stats, nil
}

func main() {
    // シミュレーション用ログファイルを作成
    logContent := `2024-01-15 08:00:01 [INFO] Service started successfully
2024-01-15 08:00:05 [DEBUG] Loading configuration file
2024-01-15 08:01:10 [WARNING] Disk space below 80%
2024-01-15 08:02:30 [ERROR] Database connection timeout
2024-01-15 08:02:35 [INFO] Retrying database connection
2024-01-15 08:03:00 [ERROR] Authentication failed: user admin
2024-01-15 08:03:01 [INFO] Request processing complete
2024-01-15 08:04:00 [DEBUG] Cache hit rate 95%
2024-01-15 08:05:00 [WARNING] API response time exceeded 2s
`
    os.WriteFile("app.log", []byte(logContent), 0644)

    // ログを分析
    stats, err := analyzeLog("app.log")
    if err != nil {
        fmt.Println("エラー:", err)
        return
    }

    // レポートを出力
    fmt.Println("========== ログ分析レポート ==========")
    fmt.Printf("総行数: %d\n", stats.Total)
    fmt.Printf("ERROR:   %d (%.1f%%)\n", stats.Error,
        float64(stats.Error)/float64(stats.Total)*100)
    fmt.Printf("WARNING: %d (%.1f%%)\n", stats.Warning,
        float64(stats.Warning)/float64(stats.Total)*100)
    fmt.Printf("INFO:    %d (%.1f%%)\n", stats.Info,
        float64(stats.Info)/float64(stats.Total)*100)
    fmt.Printf("DEBUG:   %d (%.1f%%)\n", stats.Debug,
        float64(stats.Debug)/float64(stats.Total)*100)
    fmt.Println("===========================================")

    // クリーンアップ
    os.Remove("app.log")
}
▶ 試してみよう
TEXT
========== ログ分析レポート ==========
総行数: 9
ERROR:   2 (22.2%)
WARNING: 2 (22.2%)
INFO:    3 (33.3%)
DEBUG:   2 (22.2%)
===========================================

例:ディレクトリ同期ツール(難易度⭐⭐⭐)

シンプルなディレクトリ同期関数を実装します。ソースディレクトリをスキャンし、新規または変更されたファイルを宛先ディレクトリにコピーします。

GO
package main

import (
    "fmt"
    "io"
    "os"
    "path/filepath"
    "time"
)

// FileInfoはファイル情報をキャッシュします
type FileInfo struct {
    Path    string
    ModTime time.Time
    Size    int64
}

// scanDirはディレクトリをスキャンし、ファイル情報マップを返します
func scanDir(dir string) (map[string]FileInfo, error) {
    files := make(map[string]FileInfo)

    err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
        if err != nil {
            return err
        }
        if info.IsDir() {
            return nil
        }

        // 相対パスをキーとして計算
        relPath, err := filepath.Rel(dir, path)
        if err != nil {
            return err
        }

        files[relPath] = FileInfo{
            Path:    path,
            ModTime: info.ModTime(),
            Size:    info.Size(),
        }
        return nil
    })

    return files, err
}

// copyFileDataは単一ファイルをコピーします
func copyFileData(src, dst string) error {
    srcFile, err := os.Open(src)
    if err != nil {
        return err
    }
    defer srcFile.Close()

    // 宛先ディレクトリが存在することを確認
    dstDir := filepath.Dir(dst)
    if err := os.MkdirAll(dstDir, 0755); err != nil {
        return err
    }

    dstFile, err := os.Create(dst)
    if err != nil {
        return err
    }
    defer dstFile.Close()

    _, err = io.Copy(dstFile, srcFile)
    return err
}

// syncDirはソースディレクトリを宛先ディレクトリに同期します
func syncDir(src, dst string) error {
    fmt.Printf("同期: %s → %s\n\n", src, dst)

    // 両方のディレクトリをスキャン
    srcFiles, err := scanDir(src)
    if err != nil {
        return fmt.Errorf("ソースディレクトリのスキャンに失敗しました: %w", err)
    }

    dstFiles, err := scanDir(dst)
    if err != nil && !os.IsNotExist(err) {
        return fmt.Errorf("宛先ディレクトリのスキャンに失敗しました: %w", err)
    }

    copied, updated, skipped := 0, 0, 0

    // ソースディレクトリのファイルを繰り返し処理
    for relPath, srcInfo := range srcFiles {
        dstPath := filepath.Join(dst, relPath)

        dstInfo, exists := dstFiles[relPath]

        switch {
        case !exists:
            // 新規ファイル、コピー
            fmt.Printf("  [新規] %s\n", relPath)
            if err := copyFileData(srcInfo.Path, dstPath); err != nil {
                return fmt.Errorf("%sのコピーに失敗しました: %w", relPath, err)
            }
            copied++

        case srcInfo.ModTime.After(dstInfo.ModTime) || srcInfo.Size != dstInfo.Size:
            // ファイル変更、更新
            fmt.Printf("  [更新] %s\n", relPath)
            if err := copyFileData(srcInfo.Path, dstPath); err != nil {
                return fmt.Errorf("%sの更新に失敗しました: %w", relPath, err)
            }
            updated++

        default:
            // ファイル変更なし、スキップ
            skipped++
        }
    }

    fmt.Printf("\n同期完了: 新規 %d、更新 %d、スキップ %d\n",
        copied, updated, skipped)
    return nil
}

func main() {
    // テスト用ディレクトリ構造を作成
    os.MkdirAll("src_dir/subdir", 0755)
    os.WriteFile("src_dir/main.go", []byte("package main\n"), 0644)
    os.WriteFile("src_dir/readme.txt", []byte("README\n"), 0644)
    os.WriteFile("src_dir/subdir/util.go", []byte("package util\n"), 0644)

    // 宛先ディレクトリを作成(一部のファイル付き)
    os.MkdirAll("dst_dir", 0755)
    os.WriteFile("dst_dir/readme.txt", []byte("Old README\n"), 0644)
    os.WriteFile("dst_dir/old.txt", []byte("このファイルはソースディレクトリにありません\n"), 0644)

    // 同期を実行
    err := syncDir("src_dir", "dst_dir")
    if err != nil {
        fmt.Println("同期エラー:", err)
        return
    }

    // 結果を確認
    fmt.Println("\n=== 宛先ディレクトリの内容 ===")
    filepath.Walk("dst_dir", func(path string, info os.FileInfo, err error) error {
        if err != nil {
            return err
        }
        relPath, _ := filepath.Rel("dst_dir", path)
        prefix := "📁"
        if !info.IsDir() {
            prefix = "📄"
        }
        fmt.Printf("  %s %s\n", prefix, relPath)
        return nil
    })

    // クリーンアップ
    os.RemoveAll("src_dir")
    os.RemoveAll("dst_dir")
}
▶ 試してみよう
TEXT
同期: src_dir → dst_dir

  [新規] main.go
  [更新] readme.txt
  [新規] subdir\util.go

同期完了: 新規 2、更新 1、スキップ 0

=== 宛先ディレクトリの内容 ===
  📁 .
  📁 subdir
  📄 subdir\util.go
  📄 main.go
  📄 old.txt
  📄 readme.txt

実践的な応用シナリオ

シナリオ1:設定ファイルのホットリロード

設定ファイルの変更を監視し、自動的にリロードします。

GO
package main

import (
    "encoding/json"
    "fmt"
    "os"
    "time"
)

// Config アプリケーション設定
type Config struct {
    Server   ServerConfig   `json:"server"`
    Database DatabaseConfig `json:"database"`
}

type ServerConfig struct {
    Port int    `json:"port"`
    Host string `json:"host"`
}

type DatabaseConfig struct {
    DSN         string `json:"dsn"`
    MaxOpenConn int    `json:"max_open_conn"`
}

// loadConfigはJSONファイルから設定を読み込みます
func loadConfig(filename string) (*Config, error) {
    data, err := os.ReadFile(filename)
    if err != nil {
        return nil, fmt.Errorf("設定ファイルの読み取りに失敗しました: %w", err)
    }

    var config Config
    if err := json.Unmarshal(data, &config); err != nil {
        return nil, fmt.Errorf("設定の解析に失敗しました: %w", err)
    }

    return &config, nil
}

// watchConfigは設定ファイルの変更を監視し、リロードします
func watchConfig(filename string, interval time.Duration, callback func(*Config)) {
    var lastModTime time.Time

    // 初回読み込み
    info, err := os.Stat(filename)
    if err == nil {
        lastModTime = info.ModTime()
        if config, err := loadConfig(filename); err == nil {
            callback(config)
        }
    }

    ticker := time.NewTicker(interval)
    defer ticker.Stop()

    for range ticker.C {
        info, err := os.Stat(filename)
        if err != nil {
            fmt.Printf("設定ファイルの確認に失敗しました: %v\n", err)
            continue
        }

        if info.ModTime().After(lastModTime) {
            fmt.Printf("[%s] 設定ファイルの変更を検出、リロード中...\n",
                time.Now().Format("15:04:05"))

            config, err := loadConfig(filename)
            if err != nil {
                fmt.Printf("リロード失敗: %v\n", err)
                continue
            }

            lastModTime = info.ModTime()
            callback(config)
        }
    }
}

func main() {
    // 初期設定を作成
    configJSON := `{
    "server": {
        "port": 8080,
        "host": "localhost"
    },
    "database": {
        "dsn": "user:pass@tcp(localhost:3306)/mydb",
        "max_open_conn": 25
    }
}`
    os.WriteFile("config.json", []byte(configJSON), 0644)
    defer os.Remove("config.json")

    // 設定監視を開始
    go watchConfig("config.json", 2*time.Second, func(config *Config) {
        fmt.Printf("  サーバー: %s:%d\n", config.Server.Host, config.Server.Port)
        fmt.Printf("  データベース: %s (最大接続数: %d)\n",
            config.Database.DSN, config.Database.MaxOpenConn)
    })

    // 実行をシミュレーション
    time.Sleep(3 * time.Second)

    // 設定更新をシミュレーション
    fmt.Println("\n>> 設定ファイルを更新中...")
    updatedJSON := `{
    "server": {
        "port": 9090,
        "host": "0.0.0.0"
    },
    "database": {
        "dsn": "user:pass@tcp(db-host:3306)/mydb",
        "max_open_conn": 50
    }
}`
    os.WriteFile("config.json", []byte(updatedJSON), 0644)

    // 変更検出を待機
    time.Sleep(5 * time.Second)
}
TEXT
  サーバー: localhost:8080
  データベース: user:pass@tcp(localhost:3306)/mydb (最大接続数: 25)

>> 設定ファイルを更新中...
[14:30:02] 設定ファイルの変更を検出、リロード中...
  サーバー: 0.0.0.0:9090
  データベース: user:pass@tcp(db-host:3306)/mydb (最大接続数: 50)

シナリオ2:バッチファイル名変更ツール

ディレクトリ内のファイルをルールに従って名前変更します。

GO
package main

import (
    "fmt"
    "os"
    "path/filepath"
    "strings"
)

// RenameRule 名前変更ルール
type RenameRule struct {
    Find    string // 検索文字列
    Replace string // 置換文字列
}

// batchRenameはファイルをバッチで名前変更します
func batchRename(dir string, rule RenameRule) ([]string, error) {
    entries, err := os.ReadDir(dir)
    if err != nil {
        return nil, fmt.Errorf("ディレクトリの読み取りに失敗しました: %w", err)
    }

    var renamed []string

    for _, entry := range entries {
        if entry.IsDir() {
            continue
        }

        oldName := entry.Name()
        newName := strings.ReplaceAll(oldName, rule.Find, rule.Replace)

        if oldName == newName {
            continue // 名前変更不要
        }

        oldPath := filepath.Join(dir, oldName)
        newPath := filepath.Join(dir, newName)

        // ターゲットファイルが既に存在するかチェック
        if _, err := os.Stat(newPath); err == nil {
            fmt.Printf("  [スキップ] %s → %s(ターゲットが既に存在)\n", oldName, newName)
            continue
        }

        if err := os.Rename(oldPath, newPath); err != nil {
            fmt.Printf("  [エラー] %s: %v\n", oldName, err)
            continue
        }

        fmt.Printf("  [名前変更] %s → %s\n", oldName, newName)
        renamed = append(renamed, newName)
    }

    return renamed, nil
}

func main() {
    // テストファイルを作成
    os.MkdirAll("photos", 0755)
    testFiles := []string{
        "IMG_20240115_001.jpg",
        "IMG_20240115_002.jpg",
        "IMG_20240115_003.jpg",
        "IMG_20240116_001.jpg",
        "IMG_20240116_002.jpg",
        "notes.txt",
    }
    for _, name := range testFiles {
        os.WriteFile(filepath.Join("photos", name), []byte(""), 0644)
    }

    // ルール1:プレフィックスを置換
    fmt.Println("=== ルール1:IMG → Photo ===")
    rule1 := RenameRule{Find: "IMG_", Replace: "Photo_"}
    renamed, _ := batchRename("photos", rule1)
    fmt.Printf("合計 %d ファイルの名前を変更しました\n\n", len(renamed))

    // ルール2:日付プレフィックスを追加
    fmt.Println("=== ルール2:Photo_ → 2024_Vacation_ ===")
    rule2 := RenameRule{Find: "Photo_", Replace: "2024_Vacation_"}
    renamed, _ = batchRename("photos", rule2)
    fmt.Printf("合計 %d ファイルの名前を変更しました\n", len(renamed))

    // 最終結果を表示
    fmt.Println("\n=== 最終ファイルリスト ===")
    entries, _ := os.ReadDir("photos")
    for _, entry := range entries {
        fmt.Printf("  %s\n", entry.Name())
    }

    // クリーンアップ
    os.RemoveAll("photos")
}
TEXT
=== ルール1:IMG → Photo ===
  [名前変更] IMG_20240115_001.jpg → Photo_20240115_001.jpg
  [名前変更] IMG_20240115_002.jpg → Photo_20240115_002.jpg
  [名前変更] IMG_20240115_003.jpg → Photo_20240115_003.jpg
  [名前変更] IMG_20240116_001.jpg → Photo_20240116_001.jpg
  [名前変更] IMG_20240116_002.jpg → Photo_20240116_002.jpg
合計 5 ファイルの名前を変更しました

=== ルール2:Photo_ → 2024_Vacation_ ===
  [名前変更] Photo_20240115_001.jpg → 2024_Vacation_20240115_001.jpg
  [名前変更] Photo_20240115_002.jpg → 2024_Vacation_20240115_002.jpg
  [名前変更] Photo_20240115_003.jpg → 2024_Vacation_20240115_003.jpg
  [名前変更] Photo_20240116_001.jpg → 2024_Vacation_20240116_001.jpg
  [名前変更] Photo_20240116_002.jpg → 2024_Vacation_20240116_002.jpg
合計 5 ファイルの名前を変更しました

=== 最終ファイルリスト ===
  2024_Vacation_20240115_001.jpg
  2024_Vacation_20240115_002.jpg
  2024_Vacation_20240115_003.jpg
  2024_Vacation_20240116_001.jpg
  2024_Vacation_20240116_002.jpg
  notes.txt

❓ よくある質問

質問1:大きいファイルを読み込むとメモリ使用量が急増するのはなぜですか?

GO
// ❌ 間違い:ファイル全体がメモリに読み込まれる
data, _ := os.ReadFile("huge.log") // 10GBファイル → メモリ爆発

// ✅ 正しい:行ごとに読み込む
file, _ := os.Open("huge.log")
defer file.Close()

scanner := bufio.NewScanner(file)
for scanner.Scan() {
    line := scanner.Text()
    // 一度に1行を処理、メモリ使用量は最小
    processLine(line)
}

重要なポイント: os.ReadFileは小さいファイル(< 100MB)に適しています。大きいファイルについては、常にbufio.Scannerで行ごと/チャンクごとに処理してください。

質問2:defer f.Close()err != nilチェックの前に置くとどうなりますか?

GO
// ❌ nilポインタパニックを起こす可能性がある
f, err := os.Open("file.txt")
defer f.Close() // Openが失敗した場合、fはnilで、Closeがパニックを起こす
if err != nil {
    return err
}

// ✅ 正しい:先にエラーをチェック
f, err := os.Open("file.txt")
if err != nil {
    return err
}
defer f.Close() // fがnilでないことを保証

質問3:WindowsとLinuxのパスの違いをどう処理すればいいですか?

GO
// ❌ ハードコードされたパス区切り文字
path := "data" + "/" + "file.txt"     // Linuxでは動作するが、Windowsでは危険
path := "data" + "\\" + "file.txt"    // Windowsでは動作するが、Linuxでは失敗

// ✅ filepath.Joinを使用
path := filepath.Join("data", "file.txt")

// ✅ 生の文字列を使用(Windowsパス)
path := `C:\Users\admin\file.txt`

// ✅ 絶対パスから相対パスを取得
rel, _ := filepath.Rel("/home/user", "/home/user/docs/file.txt")
// rel = "docs/file.txt"

質問4:ファイル書き込み中にデータが失われた場合はどうすればいいですか?

GO
// 原因:bufio.Writerを使用したが、Flushを忘れた
writer := bufio.NewWriter(f)
writer.WriteString("important data")
// プログラムがクラッシュまたは終了 → データはまだバッファにあり、ディスクに書き込まれていない

// ✅ 解決策1:常にdefer Flush
defer writer.Flush()

// ✅ 解決策2:重要なデータは直接書き込み
f.WriteString("important data") // 直接書き込み、バッファをバイパス

// ✅ 解決策3:書き込み直後にディスクに同期
f.Sync() // fsyncシステムコールを呼び出す

📖 まとめ

このレッスンではGoのファイルI/Oのコア知識をカバーしました:

知識ポイント 重要なポイント
os.Open/Create/Remove 低レベルファイル操作、手動でdefer Close()が必要
os.ReadFile/WriteFile Go 1.16+で推奨される一括読み書き方法
bufio.Scanner 大きいファイルを行ごとに読み込む最良の選択肢
bufio.Writer 頻繁な小さな書き込みのパフォーマンス最適化、Flush()を忘れない
filepath.Join クロスプラットフォーム対応のパス結合の正しい方法
filepath.Walk/WalkDir ディレクトリツリーの再帰的走査
io.Copy ストリーミングコピー、バッファを自動管理

コア原則:

  1. 常にdefer f.Close() — エラーチェックの後
  2. 小さいファイルはos.ReadFile、大きいファイルはbufio.Scanner
  3. パス結合はfilepath.Join — 区切り文字を手動で結合しない
  4. bufio.Writerは必ずFlush() — さもないとデータが失われる可能性がある
  5. エラー処理は省略できない — ファイル操作のエラーは特に重要

📝 演習

演習1:ワードカウンター

テキストファイルを読み込み、単語数、行数、文字数をカウントして結果を出力するプログラムを書いてください。

GO
// ヒント:
// - bufio.Scannerで行ごとに読み込む
// - strings.Fields()で各行を単語に分割
// - 3つの指標をすべてカウントしてフォーマット出力

演習2:CSV to JSON

CSVファイル(1行目がヘッダー)を読み込み、JSON配列形式に変換して新しいファイルに書き込むプログラムを書いてください。

GO
// ヒント:
// - bufio.ScannerでCSVを行ごとに読み込む
// - 1行目はJSONオブジェクトのキーとして使用
// - 以降の行は値、strings.Splitで分割
// - encoding/jsonでシリアライズして出力

演習3:ディレクトリサイズ計算機

指定されたディレクトリの合計サイズを再帰的に計算し、ファイルタイプ(拡張子)別にグループ統計するプログラムを書いてください。

GO
// ヒント:
// - filepath.WalkDirでディレクトリを走査
// - map[string]int64で拡張子別にサイズを累積
// - humanizeでフォーマットされた出力(例:1.5MB、230KB)
// - 拡張子のないファイルを処理(「拡張子なし」に分類)

次のレッスン

次のレッスンでは、JSON処理について学びます。GoでJSONデータを解析・生成する方法は、APIの構築や設定ファイルの処理に必要な基本スキルです。

👉 レッスン21:JSON処理

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%