バッチ・CLI系処理の構造化:レビューでのログ出力とexit判定の扱い
バッチ・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)呼び出し
    }
}
@ReviewerFatal()により即時終了しテスト不能構造になります。Run()内でint返却型に整理し、main()でExit判定を一元管理してください。
3. エラー出力先の曖昧化
log.Println("Error:", err)
@Reviewerlogの出力先が標準出力に向いており、stderrへ適切に誘導されていません。log.SetOutput(os.Stderr)を初期化段階で指定してください。
CLI出力・終了設計の構造分離
CLI設計崩壊フロー
CLI出力設計の責務整理パターン集
| 出力対象 | 使用関数 | 出力先 | 
|---|---|---|
| 正常系ユーザー出力 | fmt.Println() | 
stdout | 
| エラーログ | log.Println() + os.Stderr | 
stderr | 
| ログファイル出力 | log.SetOutput(io.Writer) | 
任意ファイル | 
| 終了コード制御 | Run() int → os.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.Printlnとlog.Printlnが明確に役割分離されているか | 
| エラー出力先 | log出力がstderrへ誘導されているか | 
| 終了コード制御 | Run() int → main()でExit判定設計になっているか | 
| テスト可能性 | os.Exit()が関数内に埋め込まれていないか | 
| 出力抽象化 | Loggerインターフェースを設計できているか | 
| 標準入力・引数制御 | flag, os.ArgsもDI設計になっているか | 
| ログ出口 | ログ出力先が運用に耐えうる切り替え設計か | 
まとめ:CLIレビューは「実行時障害耐性」を読み解く作業
CLIバッチ処理レビューは構造可読性・出力設計・エラー監視耐性・テスト容易性の総合評価である。
- 出力はユーザー/運用者/監視担当それぞれに正しく届けられるか?
 - 失敗終了コードはインフラと監視で正しく判定可能か?
 - テスト時に出力と終了コードは完全に検証可能構造か?
 
レビューアーは「コードは動く」で満足せず
「このCLIは障害が起きても正しく報告できるか?」まで評価を及ぼす必要がある。

