以下の内容はhttps://devlights.hatenablog.com/entry/2025/07/17/073000より取得しました。


Goメモ-595 (exec.Commandで大きな出力をストリームで扱う)(os.Pipe, bufio.Scanner.Buffer)

関連記事

GitHub - devlights/blog-summary: ブログ「いろいろ備忘録日記」のまとめ

概要

以下、自分用のメモです。

exec.Commandを使って、外部コマンドを実行して

結果を取り込んだりする際、どの言語でもそうですが実行したコマンドが大量の出力を行う場合は注意が必要です。

出力が大きすぎるとOSのバッファが一杯になって出力が詰まってしまいます。そのような場合、出力結果を一発で取得するのではなく

ストリームとして扱って順次出力結果を取得していくことが必要になります。

*exec.Cmdには、Stdin,Stdout,Stderrと必要なio.Readerとio.Writerが用意されていますので

それを利用するか、StdinPipe,StdoutPipe,StderrPipeを使って処理することも出来ますね。

*exec.Cmd関連は、以前に何個かメモかいていますので、そちらもよかったら参照ください。

今回は、自前のパイプをコマンドの出力に接続して、大きなコマンド出力を処理するサンプルです。os.Pipeの扱いに慣れたいという意味もあります。

サンプル

ospipe.go

package cmdexec

import (
    "bufio"
    "fmt"
    "io"
    "os"
    "os/exec"
)

// OsPipe は、(*Cmd).Stdout に os.Pipe の io.Writer を接続して処理するサンプルです.
//
// # REFERENCES
//   - https://pkg.go.dev/os@go1.24.4#Pipe
func OsPipe() error {
    //
    // (*Cmd).StdoutPipe()で同じことが出来るが
    // os.Pipe()の使い方を勉強するために
    // 意図的に利用している
    //

    ///////////////////////////////////
    // パイプ取得
    ///////////////////////////////////

    var (
        pr  *os.File
        pw  *os.File
        err error
    )
    if pr, pw, err = os.Pipe(); err != nil {
        return err
    }
    defer pr.Close()

    ///////////////////////////////////
    // コマンド実行
    //
    // git log コマンドを実行しているため
    // リポジトリによっては長大な出力が発生する。
    //
    // 簡易なコマンド実行である (*Cmd).Output() で取得しようとすると
    // OSのバッファが一杯になってしまう可能性があるため、このような場合は
    // ストリーミング処理が必須となる。
    //
    // 以下で実行している git コマンドのオプションは以下の通り
    //   - --no-pager  : ページャーを使用しない
    //   - log         : ログを表示
    //   - -m          : マージコミットの差分も表示
    //   - -r          : 再帰的に処理
    //   - --name-only : ファイル名のみ表示
    //   - --pretty=raw: 生フォーマットで表示
    //   - -z          : NULL文字で区切る
    ///////////////////////////////////

    var (
        name = "git"
        args = []string{"--no-pager", "log", "-m", "-r", "--name-only", "--pretty=raw", "-z"}
        cmd  = exec.Command(name, args...)
    )
    cmd.Stdout = pw
    if err = cmd.Start(); err != nil {
        pw.Close()
        return err
    }

    ///////////////////////////////////
    // 終了待機用のゴルーチンを用意
    ///////////////////////////////////

    var (
        done = make(chan error, 1)
    )
    go func() {
        defer pw.Close()
        done <- cmd.Wait()
    }()

    ///////////////////////////////////
    // コマンドの出力を読み出し
    ///////////////////////////////////

    const (
        MaxTokenSize = 1024 * 1024 // 1行のサイズが大きい可能性を考慮してバッファサイズを底上げ
    )
    var (
        scanner = bufio.NewScanner(pr)
        buf     = make([]byte, MaxTokenSize)
        count   int
    )
    scanner.Buffer(buf, MaxTokenSize)

    for scanner.Scan() {
        io.Discard.Write(scanner.Bytes())
        count++
    }

    if err = scanner.Err(); err != nil {
        return err
    }

    ///////////////////////////////////
    // コマンド終了待機
    ///////////////////////////////////

    if err = <-done; err != nil {
        return err
    }

    ///////////////////////////////////
    // 結果出力
    ///////////////////////////////////

    fmt.Printf("Total lines: %d\n", count)

    return nil
}

実行結果

$ task
task: [build] go build -o "/workspace/try-golang/try-golang" .
task: [run] ./try-golang -onetime

ENTER EXAMPLE NAME: cmdexec_ospipe

[Name] "cmdexec_ospipe"
Total lines: 29996


[Elapsed] 122.721215ms

自前のリポジトリですが、結構なコミット数があるので git log すると大量の出力が流れます。

$ git rev-list --count HEAD
2288

参考情報

os package - os - Go Packages

exec package - os/exec - Go Packages

Goのおすすめ書籍


過去の記事については、以下のページからご参照下さい。

サンプルコードは、以下の場所で公開しています。




以上の内容はhttps://devlights.hatenablog.com/entry/2025/07/17/073000より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14