Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 14956] ファイルの概要

このコミットは、Go言語のコマンドラインツール go list において、特定のフォーマットオプション (-f) を使用した際に発生していた余分な改行(newline)を抑制するための修正です。具体的には、出力の最後に不要な改行が追加される問題を解決し、よりクリーンで予測可能な出力を実現しています。

コミット

cmd/go: suppress extraneous newlines in list

このコミットは、go list コマンドがテンプレートエンジンを使用してパッケージ情報を出力する際に、不必要な改行が出力される問題を修正します。以前は、出力が全くない場合でも改行が追加されたり、複数のパッケージをリストする際にパッケージ間に余分な改行が挿入されたりしていました。この変更により、go list -f の出力がより簡潔になり、スクリプトなどでの利用が容易になります。

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/e6e60cda12701ad3f1f4419606ddead52e57f2f1

元コミット内容

commit e6e60cda12701ad3f1f4419606ddead52e57f2f1
Author: Anthony Martin <ality@pbrane.org>
Date:   Tue Jan 22 17:05:13 2013 -0500

    cmd/go: suppress extraneous newlines in list
    
    Before:
    $ go list -f '{{range .Deps}}{{println $.Name .}}{{end}}' math time
    math runtime
    math unsafe
    
    time errors
    time runtime
    time sync
    time sync/atomic
    time syscall
    time unsafe
    
    $
    
    After:
    $ go list -f '{{range .Deps}}{{println $.Name .}}{{end}}' math time
    math runtime
    math unsafe
    time errors
    time runtime
    time sync
    time sync/atomic
    time syscall
    time unsafe
    $
    
    R=minux.ma, rsc
    CC=golang-dev
    https://golang.org/cl/7130052

変更の背景

go list コマンドは、Goパッケージに関する情報を表示するための強力なツールです。特に -f フラグを使用すると、Goの text/template パッケージの構文を用いて、出力フォーマットを自由にカスタマイズできます。この柔軟性により、ユーザーはパッケージの依存関係、ビルド情報、ファイルパスなど、さまざまな情報を抽出できます。

しかし、このカスタマイズ機能には一つ問題がありました。テンプレートの実行結果が io.Writer に書き込まれる際、出力の最後に常に改行が追加されるという挙動です。これは、テンプレートが何も出力しなかった場合でも発生し、また複数のパッケージを処理する際に、各パッケージの出力の間に余分な空行が挿入される原因となっていました。

上記の「元コミット内容」の Before:After: の比較を見ると、math パッケージの出力と time パッケージの出力の間に空行が挿入されていること、そして全体の出力の最後に余分な改行があることがわかります。このような余分な改行は、go list の出力をシェルスクリプトや他のプログラムで処理する際に、パースエラーや不必要な処理を引き起こす可能性がありました。このコミットは、この「余分な改行」という問題を解決し、go list の出力をより予測可能で扱いやすいものにすることを目的としています。

前提知識の解説

このコミットを理解するためには、以下のGo言語および関連技術の概念を理解しておく必要があります。

  1. go list コマンド: go list はGo言語のツールチェーンの一部であり、Goパッケージに関する情報を表示するために使用されます。

    • 基本機能: 指定されたパッケージのパス、モジュール情報、依存関係などを表示します。
    • -f フラグ: このフラグを使用すると、Goの text/template パッケージの構文を使って出力フォーマットをカスタマイズできます。例えば、go list -f '{{.Deps}}' mypackagemypackage の直接の依存関係をリストします。
    • .Name.Deps: テンプレート内でアクセスできるパッケージ構造体 (Package 型) のフィールドです。.Name はパッケージ名、.Deps はそのパッケージが直接依存しているパッケージのリストを表します。
    • println 関数: テンプレート内で使用できる関数で、引数を標準出力に書き込み、その後に改行を追加します。
  2. io.Writer インターフェース: Go言語における基本的なI/Oインターフェースの一つです。データを書き込むための抽象化を提供します。

    • Write(p []byte) (n int, err error) メソッドを持ち、バイトスライス p を書き込み、書き込んだバイト数 n とエラー err を返します。
    • os.Stdoutio.Writer インターフェースを実装しており、標準出力への書き込みに使用されます。
  3. bufio.Writer: io.Writer をラップし、バッファリング機能を追加する型です。

    • 書き込み操作をバッファリングすることで、I/Oの効率を向上させます。
    • Flush() メソッドを呼び出すことで、バッファに溜まったデータを強制的に基になる io.Writer に書き出します。
  4. Goの text/template パッケージ: Go言語に組み込まれているテキストテンプレートエンジンです。

    • データ構造をテンプレートに渡して、その構造体のフィールドやメソッドにアクセスし、動的にテキストを生成できます。
    • Execute(wr io.Writer, data interface{}) error メソッドは、指定された io.Writer にテンプレートの実行結果を書き込みます。
  5. 改行の扱い: Unix系システムでは、改行は通常 \n (ラインフィード) で表されます。テキスト処理において、余分な改行はしばしば問題を引き起こします。

これらの概念を理解することで、コミットがどのように go list の出力挙動を改善しているのか、その技術的な詳細をより深く把握できます。

技術的詳細

このコミットの核心は、go list コマンドの出力処理において、CountingWriter というカスタム io.Writer の実装を TrackingWriter という新しい実装に置き換えた点にあります。

CountingWriter の問題点

以前の CountingWriter は、書き込まれたバイト数を count フィールドで追跡していました。そして、テンプレートの実行後に out.Count() > 0 (何か出力があった場合) にのみ改行を追加していました。

// 旧コード (list.go)
func runList(cmd *Command, args []string) {
    out := newCountingWriter(os.Stdout) // CountingWriter を使用
    // ...
    do = func(p *Package) {
        out.Reset() // 各パッケージ処理前にカウントをリセット
        if err := tmpl.Execute(out, p); err != nil {
            // ...
        }
        if out.Count() > 0 { // 何か出力があったら改行を追加
            out.w.WriteRune('\n')
        }
    }
    // ...
}

// CountingWriter の定義 (旧コード)
type CountingWriter struct {
    w     *bufio.Writer
    count int64
}

func (cw *CountingWriter) Write(p []byte) (n int, err error) {
    cw.count += int64(len(p)) // 書き込んだバイト数をカウント
    return cw.w.Write(p)
}

func (cw *CountingWriter) Reset() {
    cw.count = 0 // カウントをリセット
}

func (cw *CountingWriter) Count() int64 {
    return cw.count
}

このアプローチの問題点は以下の通りです。

  1. println 関数による改行: go list -f '{{range .Deps}}{{println $.Name .}}{{end}}' のようにテンプレート内で println 関数を使用すると、println 自体が改行を出力します。CountingWriterprintln が出力した改行を単なるバイトとしてカウントするだけで、それが「改行」であるかどうかを区別しませんでした。
  2. 余分な改行の発生: 各パッケージの処理の最後に if out.Count() > 0 { out.w.WriteRune('\n') } が実行されます。もしテンプレートが println で既に改行を出力していたとしても、CountingWriter はそのことを認識せず、さらに追加の改行を挿入してしまう可能性がありました。特に、複数のパッケージを処理する際に、各パッケージの出力の間に余分な空行が生まれる原因となっていました。また、テンプレートが何も出力しなかった場合でも、out.Count() が0であれば改行は追加されませんが、これは出力の最後に余分な改行が残る問題とは別です。

TrackingWriter による解決

新しい TrackingWriter は、書き込まれた最後のバイトを last フィールドで追跡します。これにより、出力の最後に改行が必要かどうかをより正確に判断できるようになります。

// 新コード (list.go)
func runList(cmd *Command, args []string) {
    out := newTrackingWriter(os.Stdout) // TrackingWriter を使用
    defer out.w.Flush() // Flush は TrackingWriter のメソッドではなく、内部の bufio.Writer のメソッドを直接呼び出す
    // ...
    do = func(p *Package) {
        // out.Reset() は不要になった
        if err := tmpl.Execute(out, p); err != nil {
            out.Flush() // エラー時はバッファをフラッシュ
            fatalf("%s", err)
        }
        if out.NeedNL() { // 改行が必要かどうかの新しいチェック
            out.Write([]byte{'\n'}) // 必要なら改行を追加
        }
    }
    // ...
}

// TrackingWriter の定義 (新コード)
type TrackingWriter struct {
    w    *bufio.Writer
    last byte // 最後に書き込まれたバイト
}

func newTrackingWriter(w io.Writer) *TrackingWriter {
    return &TrackingWriter{
        w:    bufio.NewWriter(w),
        last: '\n', // 初期値は改行。これにより、最初の出力の前には改行が不要と判断される
    }
}

func (t *TrackingWriter) Write(p []byte) (n int, err error) {
    n, err = t.w.Write(p)
    if n > 0 {
        t.last = p[n-1] // 最後に書き込まれたバイトを更新
    }
    return
}

func (t *TrackingWriter) Flush() {
    t.w.Flush()
}

func (t *TrackingWriter) NeedNL() bool {
    return t.last != '\n' // 最後のバイトが改行でなければ、改行が必要
}

TrackingWriter の主な改善点は以下の通りです。

  • last バイトの追跡: Write メソッドが呼び出されるたびに、書き込まれたバイトスライスの最後のバイトを t.last に保存します。
  • NeedNL() メソッド: この新しいメソッドは、t.last が改行文字 (\n) でない場合に true を返します。つまり、直前の出力が改行で終わっていない場合にのみ、追加の改行が必要であると判断します。
  • 初期値の工夫: newTrackingWritert.last\n で初期化することで、最初のパッケージの出力の前には改行が不要であると正しく判断されます。
  • Reset() の廃止: CountingWriter のように各パッケージ処理前にカウントをリセットする必要がなくなりました。TrackingWriter は常に直前の書き込みの状態のみを考慮するため、連続する出力に対して適切に動作します。

この変更により、println 関数がテンプレート内で改行を出力した場合でも、TrackingWriter はそれを認識し、NeedNL()false を返すため、余分な改行が追加されることがなくなりました。結果として、go list -f の出力は、ユーザーが意図した通りの、余分な改行を含まないクリーンなものになります。

コアとなるコードの変更箇所

このコミットにおける主要なコード変更は、src/cmd/go/list.go ファイルに集中しています。

  1. newCountingWriter から newTrackingWriter への変更: runList 関数内で、出力ライターの初期化が newCountingWriter(os.Stdout) から newTrackingWriter(os.Stdout) に変更されました。

    --- a/src/cmd/go/list.go
    +++ b/src/cmd/go/list.go
    @@ -99,7 +99,7 @@ var listJson = cmdList.Flag.Bool("json", false, "")
     var nl = []byte{'\n'}
     
     func runList(cmd *Command, args []string) {
    -	out := newCountingWriter(os.Stdout)
    +	out := newTrackingWriter(os.Stdout)
     	defer out.w.Flush()
     
     	var do func(*Package)
    
  2. 出力後の改行追加ロジックの変更: テンプレート実行後の改行追加の条件が out.Count() > 0 から out.NeedNL() に変更され、out.Reset() の呼び出しが削除されました。

    --- a/src/cmd/go/list.go
    +++ b/src/cmd/go/list.go
    @@ -119,13 +119,12 @@ func runList(cmd *Command, args []string) {
     		tfatalf("%s", err)
     	}
     	do = func(p *Package) {
    -		out.Reset()
     		if err := tmpl.Execute(out, p); err != nil {
     			out.Flush()
     			fatalf("%s", err)
     		}
    -		if out.Count() > 0 {
    -			out.w.WriteRune('\n')
    +		if out.NeedNL() {
    +			out.Write([]byte{'\n'})
     		}
     	}
     }
    
  3. CountingWriter 型の削除と TrackingWriter 型の追加: CountingWriter の定義が削除され、代わりに TrackingWriter が新しい構造体として定義されました。これに伴い、関連するメソッド (Write, Flush, NeedNL) も実装されています。

    --- a/src/cmd/go/list.go
    +++ b/src/cmd/go/list.go
    @@ -140,32 +139,33 @@ func runList(cmd *Command, args []string) {
     	}
     }
     
    -// CountingWriter counts its data, so we can avoid appending a newline
    -// if there was no actual output.
    -type CountingWriter struct {
    -	w     *bufio.Writer
    -	count int64
    +// TrackingWriter tracks the last byte written on every write so
    +// we can avoid printing a newline if one was already written or
    +// if there is no output at all.
    +type TrackingWriter struct {
    +	w    *bufio.Writer
    +	last byte
    }
     
    -func newCountingWriter(w io.Writer) *CountingWriter {
    -	return &CountingWriter{
    -		w: bufio.NewWriter(w),
    +func newTrackingWriter(w io.Writer) *TrackingWriter {
    +	return &TrackingWriter{
    +		w:    bufio.NewWriter(w),
    +		last: '\n',
     	}
     }
     
    -func (cw *CountingWriter) Write(p []byte) (n int, err error) {
    -	cw.count += int64(len(p))
    -	return cw.w.Write(p)
    -}
    -
    -func (cw *CountingWriter) Flush() {
    -	cw.w.Flush()
    +func (t *TrackingWriter) Write(p []byte) (n int, err error) {
    +	n, err = t.w.Write(p)
    +	if n > 0 {
    +		t.last = p[n-1]
    +	}
    +	return
     }
     
    -func (cw *CountingWriter) Reset() {
    -	cw.count = 0
    +func (t *TrackingWriter) Flush() {
    +	t.w.Flush()
     }
     
    -func (cw *CountingWriter) Count() int64 {
    -	return cw.count
    +func (t *TrackingWriter) NeedNL() bool {
    +	return t.last != '\n'
     }
    

コアとなるコードの解説

このコミットの核心は、TrackingWriter という新しいカスタム io.Writer の実装にあります。

TrackingWriter 構造体

type TrackingWriter struct {
	w    *bufio.Writer
	last byte
}
  • w *bufio.Writer: 実際の書き込みを行うための内部バッファ付きライターです。TrackingWriter はこの bufio.Writer をラップして、追加のロジックを提供します。
  • last byte: このフィールドが TrackingWriter の主要な機能を提供します。これは、Write メソッドが最後に書き込んだバイトを保持します。このバイトを追跡することで、出力の最後に改行が必要かどうかを判断できます。

newTrackingWriter 関数

func newTrackingWriter(w io.Writer) *TrackingWriter {
	return &TrackingWriter{
		w:    bufio.NewWriter(w),
		last: '\n', // 初期値は改行
	}
}
  • io.Writer を引数に取り、新しい TrackingWriter のインスタンスを返します。
  • 内部の bufio.Writer を初期化します。
  • last フィールドを \n (改行文字) で初期化している点が重要です。これにより、TrackingWriter が作成された直後(つまり、まだ何も書き込まれていない状態)では、NeedNL() メソッドは false を返します。これは、最初の出力の前に余分な改行が追加されるのを防ぐための賢い初期設定です。

Write メソッド

func (t *TrackingWriter) Write(p []byte) (n int, err error) {
	n, err = t.w.Write(p)
	if n > 0 {
		t.last = p[n-1] // 最後に書き込まれたバイトを更新
	}
	return
}
  • io.Writer インターフェースの Write メソッドを実装しています。
  • 引数 p のバイトスライスを内部の bufio.Writer (t.w) に書き込みます。
  • 書き込みが成功し、かつ1バイト以上書き込まれた場合 (n > 0)、書き込まれたバイトスライスの最後のバイト (p[n-1]) を t.last に保存します。これにより、常に最新の出力の最後の文字が追跡されます。

Flush メソッド

func (t *TrackingWriter) Flush() {
	t.w.Flush()
}
  • 内部の bufio.WriterFlush メソッドを呼び出します。これにより、バッファに溜まっているデータが基になる io.Writer (この場合は os.Stdout) に強制的に書き出されます。

NeedNL メソッド

func (t *TrackingWriter) NeedNL() bool {
	return t.last != '\n'
}
  • このメソッドは、現在の出力ストリームの最後に改行が必要かどうかを判断します。
  • t.last が改行文字 (\n) でない場合に true を返します。つまり、直前の書き込みが改行で終わっていなければ、追加の改行が必要であると判断します。
  • もし t.last\n であれば、直前の出力は既に改行で終わっているため、追加の改行は不要と判断し false を返します。

この TrackingWriter の導入により、go list コマンドは、テンプレートからの出力が既に改行を含んでいるかどうかを正確に判断し、必要に応じてのみ追加の改行を挿入するようになりました。これにより、出力の冗長性が排除され、よりクリーンで予測可能な結果が得られるようになりました。

関連リンク

参考にした情報源リンク