os操作を含む副作用抽象化レビュー完全ガイド

Go言語で os パッケージは最も設計崩壊を引き起こしやすい危険領域である。
レビューアーは「動くコード」ではなく、副作用抽象化・依存分離・差し替え容易性を基準に構造評価を行う必要がある。

本稿は実務での os 操作コードに対するレビュー観点を、副作用分離・設計責務整理の観点から体系的に解説する実務レビューガイドである。


良い実装例:副作用を責務分離・抽象化した構造

type FileWriter interface {
    Write(path string, data []byte) error
}

type OSFileWriter struct{}

func (w *OSFileWriter) Write(path string, data []byte) error {
    return os.WriteFile(path, data, 0644)
}

type LogSaver struct {
    writer FileWriter
}

func NewLogSaver(w FileWriter) *LogSaver {
    return &LogSaver{writer: w}
}

func (s *LogSaver) Save(path, data string) error {
    return s.writer.Write(path, []byte(data))
}
  • 依存注入による外部依存切り離し
  • 実環境とテスト環境での差し替え容易性
  • 責務分離により保守性・変更耐性が高い設計

良くない実装例

1. ファイルパス直埋め+副作用直呼び出し

func SaveLog(data string) error {
    return os.WriteFile("/var/log/app.log", []byte(data), 0644)
}
@Reviewer
ファイルパスが直埋めで環境切替不能です。副作用抽象化が行われておらず、テスト容易性も著しく低下します。FileWriterインターフェース分離を検討してください。

2. os.Getenvの直参照構造

func LoadConfig() string {
    return os.Getenv("CONFIG_PATH")
}
@Reviewer
環境変数への直依存により、テスト時の環境差異を直接持ち込みます。EnvProviderインターフェース設計に切り出し、差し替え可能構造へ移行してください。

3. カレントディレクトリ依存の危険設計

func WriteFile(filename string, data []byte) error {
    cwd, _ := os.Getwd()
    path := filepath.Join(cwd, filename)
    return os.WriteFile(path, data, 0644)
}
@Reviewer
実行ディレクトリ依存により環境間で動作差異が発生します。呼び出し側から絶対パスを渡す設計へ変更してください。

4. ファイルハンドル解放漏れ

func ReadFile(path string) ([]byte, error) {
    f, err := os.Open(path)
    if err != nil {
        return nil, err
    }
    // defer f.Close()忘れ
    return io.ReadAll(f)
}
@Reviewer
defer f.Close()が欠落しています。ファイルディスクリプタリークの原因となります。リソース解放は最優先確認対象です。

副作用抽象化の責務分離モデル

UML Diagram

副作用レビューでの典型的依存関係の悪化フロー

UML Diagram

よくある抽象設計パターン集

構造 抽象インターフェース
ファイル出力 FileWriter.Write(path, data)
環境変数取得 EnvProvider.Get(key)
現在時刻取得 Clock.Now()
UUID生成 UUIDProvider.New()
プロセス情報取得 ProcessInfo.GetPID()

レビューでは os利用を直接許可しない方針を取る実務現場も多い。副作用抽象を標準構成として整備する設計文化が有効。


テスト容易性の確保:差し替え例

type MockFileWriter struct {
    LastPath string
    LastData []byte
}

func (m *MockFileWriter) Write(path string, data []byte) error {
    m.LastPath = path
    m.LastData = data
    return nil
}

func TestLogSaver(t *testing.T) {
    mockWriter := &MockFileWriter{}
    saver := NewLogSaver(mockWriter)

    err := saver.Save("test.log", "sample")
    if err != nil {
        t.Fatal(err)
    }

    if mockWriter.LastPath != "test.log" {
        t.Errorf("unexpected path: %v", mockWriter.LastPath)
    }
}
  • 実ファイル出力無しで副作用検査可能
  • Mock実装の差し替え容易性が構造上保証される

副作用抽象化レビュー観点チェックリスト

観点 チェック内容
ファイル操作抽象 os.WriteFile直参照排除
環境変数抽象 os.Getenv直参照排除
カレントディレクトリ依存 Getwd依存排除
リソース解放 defer Close漏れ確認
インターフェース分離 FileWriter等による抽象化
テスト容易性 Mock差し替え可能構造
副作用責務集中 副作用アクセス箇所の集中管理設計

まとめ:副作用レビューは「外部依存の統制力」を問う

副作用抽象設計は、レビューアーの設計意図読解力と実務統制力が試される領域である。

  • 副作用を抽象インターフェース化し構造依存を分離できているか
  • テスト・本番・CIすべてで同一構造が成立する設計か
  • 責務がファイル・環境変数・プロセスで混在せず集中管理されているか

副作用を「隠蔽」せず「抽象化」する。
この違いをレビューで見抜き、改善指導できるかがレビューアーの設計力である。