バッチ・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は障害が起きても正しく報告できるか?」まで評価を及ぼす必要がある。