バッチ・CLI系処理の構造化:レビューでのログ出力とexit判定の扱い

Go言語でCLIやバッチ系処理を実装する際は、以下の設計崩壊ポイントが頻出する。

  • ログ出力と標準出力の混在
  • エラーハンドリング曖昧化
  • os.Exit の乱用によるテスト困難化
  • 出力先ハードコードによる監視障害

レビューアーは 出力設計・責務分離・終了制御 を徹底的に構造評価する必要がある。

本稿はGoでのバッチ処理レビュー観点を「構造」「出力責務」「終了判定」「テスト可能性」の4軸で整理した実務レビューガイドである。


良い設計例:構造分離と出力抽象化を成立させたCLI実装

type Logger interface {
    Info(msg string)
    Error(msg string)
}

type StdLogger struct{}

func (l *StdLogger) Info(msg string)  { log.Println("[INFO]", msg) }
func (l *StdLogger) Error(msg string) { log.Println("[ERROR]", msg) }

type CLI struct {
    logger Logger
}

func (c *CLI) Run() int {
    c.logger.Info("処理開始")
    err := doTask()
    if err != nil {
        c.logger.Error(fmt.Sprintf("処理失敗: %v", err))
        return 1
    }
    fmt.Println("正常終了")
    return 0
}

func main() {
    cli := &CLI{logger: &StdLogger{}}
    os.Exit(cli.Run())
}
  • 責務分離(ロジックはCLI型へ集約)
  • 出力分離(標準出力/エラーログ/ログ抽象化)
  • 終了コード返却型(Run()→int)
  • テスト容易性(Logger差し替え可能構造)

良くない実装例とレビュー指摘

1. fmt.Println濫用によるログ・出力混在

func Run() {
    fmt.Println("開始")
    err := doTask()
    if err != nil {
        fmt.Println("エラー:", err)
        os.Exit(1)
    }
    fmt.Println("完了")
}
@Reviewer
ログと標準出力が区別されず混在しています。ログ責務と標準出力は明確に分離してください。logパッケージ+標準エラー出力を導入しましょう。

2. os.Exitの埋め込みによるテスト困難化

func Process() {
    err := task()
    if err != nil {
        log.Fatal(err)  // 内部でos.Exit(1)呼び出し
    }
}
@Reviewer
Fatal()により即時終了しテスト不能構造になります。Run()内でint返却型に整理し、main()でExit判定を一元管理してください。

3. エラー出力先の曖昧化

log.Println("Error:", err)
@Reviewer
logの出力先が標準出力に向いており、stderrへ適切に誘導されていません。log.SetOutput(os.Stderr)を初期化段階で指定してください。

CLI出力・終了設計の構造分離

UML Diagram

CLI設計崩壊フロー

UML Diagram

CLI出力設計の責務整理パターン集

出力対象 使用関数 出力先
正常系ユーザー出力 fmt.Println() stdout
エラーログ log.Println() + os.Stderr stderr
ログファイル出力 log.SetOutput(io.Writer) 任意ファイル
終了コード制御 Run() intos.Exit(n) main関数

レビューでは「print」「log」「exit」3領域が混在しない設計を常に要求すべき。


テスト可能性確保:出力・終了コードの差し替え例

type MockLogger struct {
    Logs []string
}

func (m *MockLogger) Info(msg string)  { m.Logs = append(m.Logs, "[INFO] "+msg) }
func (m *MockLogger) Error(msg string) { m.Logs = append(m.Logs, "[ERROR] "+msg) }

func TestRun(t *testing.T) {
    logger := &MockLogger{}
    cli := &CLI{logger: logger}

    exitCode := cli.Run()

    if exitCode != 0 {
        t.Errorf("unexpected exit code: %d", exitCode)
    }
    if len(logger.Logs) == 0 {
        t.Error("logs missing")
    }
}
  • os.Exit完全排除
  • 終了コード検証可能
  • 出力内容の検証容易

CLIレビュー観点チェックリスト

項目 内容
出力責務分離 fmt.Printlnlog.Printlnが明確に役割分離されているか
エラー出力先 log出力がstderrへ誘導されているか
終了コード制御 Run() intmain()でExit判定設計になっているか
テスト可能性 os.Exit()が関数内に埋め込まれていないか
出力抽象化 Loggerインターフェースを設計できているか
標準入力・引数制御 flag, os.ArgsもDI設計になっているか
ログ出口 ログ出力先が運用に耐えうる切り替え設計か

まとめ:CLIレビューは「実行時障害耐性」を読み解く作業

CLIバッチ処理レビューは構造可読性・出力設計・エラー監視耐性・テスト容易性の総合評価である。

  • 出力はユーザー/運用者/監視担当それぞれに正しく届けられるか?
  • 失敗終了コードはインフラと監視で正しく判定可能か?
  • テスト時に出力と終了コードは完全に検証可能構造か?

レビューアーは「コードは動く」で満足せず
「このCLIは障害が起きても正しく報告できるか?」まで評価を及ぼす必要がある。