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

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

このコミットは、Go言語のコマンドラインツール go list において、出力がない場合に余分な空行が印字される問題を修正するものです。特に、go list -f "..." のようにカスタムフォーマットを指定し、そのフォーマットが特定の条件で何も出力しない場合に、大量の空行が生成されるのを防ぐための変更が加えられました。

コミット

commit 1086dd7cfb70e382d6bb3242d26e7f673fffb808
Author: Rob Pike <r@golang.org>
Date:   Sat Feb 25 08:00:55 2012 +1100

    cmd/go: in list, don't print blank lines for no output
    Otherwise
            go list -f "{{if .Stale}}{{.ImportPath}}{{end}}" all
    and similar commands can print pages of empty lines.
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/5696058

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

https://github.com/golang/go/commit/1086dd7cfb70e382d6bb3242d26e7f673fffb808

元コミット内容

go list コマンドは、Goパッケージの情報を表示するためのツールです。-f フラグを使用すると、text/template パッケージのテンプレート構文を用いて出力フォーマットをカスタマイズできます。 このコミット以前は、go list -f "..." のようにカスタムフォーマットを指定した場合、テンプレートの評価結果が空文字列であっても、go list は常に改行を出力していました。これにより、例えば go list -f "{{if .Stale}}{{.ImportPath}}{{end}}" all のように、特定の条件(この例ではパッケージが古い場合)でのみ出力を行うテンプレートを使用すると、条件に合致しないパッケージに対しては空行が大量に生成され、出力が非常に読みにくくなるという問題がありました。

変更の背景

go list コマンドは、スクリプトや他のツールからGoパッケージの情報をプログラム的に取得する際に非常に強力な機能を提供します。しかし、前述の空行の問題は、特に大量のパッケージを処理する場合や、特定の条件に合致するパッケージのみを抽出したい場合に、出力のパースを困難にし、無駄な出力を生み出していました。 この変更は、go list の出力をよりクリーンにし、プログラムによる処理を容易にすることを目的としています。出力が実際にあった場合にのみ改行を付加することで、ユーザー体験とスクリプトの堅牢性を向上させます。

前提知識の解説

  • go list コマンド: Go言語のビルドシステムの一部であり、Goパッケージに関する情報を表示するために使用されます。パッケージのパス、依存関係、ビルド情報など、様々なメタデータにアクセスできます。
  • text/template パッケージ: Go言語の標準ライブラリの一部で、テキストベースのテンプレートを生成するための機能を提供します。HTML、XML、プレーンテキストなど、様々な形式の出力を動的に生成するのに使われます。go list -f で使用されるテンプレートエンジンもこれに基づいています。
  • bufio.Writer: io.Writer インターフェースを実装するバッファリングされたライターです。書き込み操作をバッファリングすることで、I/Oの効率を向上させます。通常、os.Stdout のような低レベルのライターをラップして使用されます。
  • io.Writer インターフェース: Go言語の標準ライブラリ io パッケージで定義されているインターフェースで、データを書き込むための抽象化を提供します。Write([]byte) (n int, err error) メソッドを持ちます。

技術的詳細

このコミットの核心は、go list がテンプレートの評価結果を標準出力に書き込む際に、実際にデータが書き込まれたかどうかを追跡する新しい CountingWriter 型を導入した点にあります。

従来の go list の実装では、text/templateExecute メソッドが bufio.Writer に直接書き込みを行い、その後、無条件に改行文字 (\n) を出力していました。このため、テンプレートが何も出力しなくても改行だけが印字されていました。

新しいアプローチでは、以下の変更が行われました。

  1. CountingWriter の導入:

    • CountingWriterbufio.Writer を内部に持ち、io.Writer インターフェースを実装します。
    • Write メソッドが呼び出されるたびに、書き込まれたバイト数を count フィールドに加算します。
    • Reset メソッドを提供し、count を0にリセットできるようにします。これは、各パッケージの処理を開始する前に、そのパッケージの出力カウントをリセットするために使用されます。
    • Count メソッドを提供し、現在の書き込みバイト数を取得できるようにします。
  2. go list の出力ロジックの変更:

    • runList 関数内で、os.Stdout を直接 bufio.NewWriter でラップする代わりに、newCountingWriter(os.Stdout) を使用して CountingWriter のインスタンスを作成します。
    • テンプレートのパース時に、*listFmt + "\n" ではなく、*listFmt のみをパースするように変更されました。これにより、テンプレート自体が改行を出力しないようになります。
    • 各パッケージの処理ループ内で、テンプレートを実行する前に out.Reset() を呼び出し、そのパッケージの出力カウントをリセットします。
    • テンプレートの実行後、out.Count() > 0 をチェックし、実際に何らかのデータが書き込まれた場合にのみ out.w.WriteRune('\n') を呼び出して改行を出力します。

この変更により、テンプレートが空文字列を生成した場合、CountingWritercount は0のままであり、結果として余分な改行は出力されなくなります。

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

src/cmd/go/list.go ファイルに以下の変更が加えられました。

--- a/src/cmd/go/list.go
+++ b/src/cmd/go/list.go
@@ -7,6 +7,7 @@ package main
 import (
 	"bufio"
 	"encoding/json"
+	"io" // 追加
 	"os"
 	"text/template"
 )
@@ -82,8 +83,8 @@ var listJson = cmdList.Flag.Bool("json", false, "")
 var nl = []byte{'\n'}
 
 func runList(cmd *Command, args []string) {
-	out := bufio.NewWriter(os.Stdout)
-	defer out.Flush()
+	out := newCountingWriter(os.Stdout) // CountingWriterを使用
+	defer out.w.Flush() // 内部のbufio.WriterのFlushを呼び出す
 
 	var do func(*Package)
 	if *listJson {
@@ -97,15 +98,19 @@ func runList(cmd *Command, args []string) {
 			out.Write(nl)
 		}
 	} else {
-		tmpl, err := template.New("main").Parse(*listFmt + "\n") // 改行をテンプレートから削除
+		tmpl, err := template.New("main").Parse(*listFmt)
 		if err != nil {
 			fatalf("%s", err)
 		}
 		do = func(p *Package) {
-			if err := tmpl.Execute(out, p); err != nil {
+			out.Reset() // 各パッケージ処理前にカウントをリセット
+			if err := tmpl.Execute(out, p); err != nil { // CountingWriterに書き込む
 				out.Flush()
 				fatalf("%s", err)
 			}
+			if out.Count() > 0 { // 出力があった場合のみ改行
+				out.w.WriteRune('\n')
+			}
 		}
 	}
 
@@ -118,3 +123,33 @@ func runList(cmd *Command, args []string) {
 		do(pkg)
 	}
 }
+
+// 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
+}
+
+func newCountingWriter(w io.Writer) *CountingWriter {
+	return &CountingWriter{
+		w: bufio.NewWriter(w),
+	}
+}
+
+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 (cw *CountingWriter) Reset() {
+	cw.count = 0
+}
+
+func (cw *CountingWriter) Count() int64 {
+	return cw.count
+}

コアとなるコードの解説

CountingWriter 構造体

type CountingWriter struct {
	w     *bufio.Writer
	count int64
}
  • w *bufio.Writer: 実際の書き込みを行う bufio.Writer のインスタンスを保持します。これは、標準出力へのバッファリングされた書き込みを処理します。
  • count int64: Write メソッドが呼び出されるたびに、書き込まれたバイト数を累積するカウンターです。

newCountingWriter 関数

func newCountingWriter(w io.Writer) *CountingWriter {
	return &CountingWriter{
		w: bufio.NewWriter(w),
	}
}
  • io.Writer を受け取り、それを bufio.NewWriter でラップして CountingWriter の新しいインスタンスを返します。これにより、CountingWriter は任意の io.Writer に対応できるようになります。

Write メソッド

func (cw *CountingWriter) Write(p []byte) (n int, err error) {
	cw.count += int64(len(p))
	return cw.w.Write(p)
}
  • io.Writer インターフェースの Write メソッドを実装します。
  • 引数 p の長さ (len(p)) を cw.count に加算し、書き込まれたバイト数を追跡します。
  • 実際の書き込みは内部の cw.w.Write(p) に委譲します。

Flush メソッド

func (cw *CountingWriter) Flush() {
	cw.w.Flush()
}
  • 内部の bufio.WriterFlush メソッドを呼び出し、バッファリングされたデータを強制的に基になる io.Writer (この場合は os.Stdout) に書き出します。

Reset メソッド

func (cw *CountingWriter) Reset() {
	cw.count = 0
}
  • count フィールドを0にリセットします。これは、各パッケージの出力処理を開始する前に呼び出され、そのパッケージの出力が実際にあったかどうかを正確に判断するために使用されます。

Count メソッド

func (cw *CountingWriter) Count() int64 {
	return cw.count
}
  • 現在の count の値(書き込まれたバイト数)を返します。この値が0より大きい場合にのみ改行を出力するというロジックで使用されます。

runList 関数内の変更

  • out := newCountingWriter(os.Stdout): os.Stdout をラップする CountingWriter が作成され、out 変数に割り当てられます。
  • defer out.w.Flush(): runList 関数が終了する際に、内部の bufio.Writer のバッファをフラッシュするように変更されました。
  • tmpl, err := template.New("main").Parse(*listFmt): テンプレート文字列から無条件の改行が削除されました。これにより、テンプレート自体は改行を出力しなくなります。
  • out.Reset(): 各パッケージのテンプレートを実行する直前に呼び出され、そのパッケージの出力バイト数をリセットします。
  • if out.Count() > 0 { out.w.WriteRune('\n') }: テンプレートの実行後、CountingWriterCount() メソッドをチェックし、もし出力が1バイトでもあった場合(count > 0)、内部の bufio.Writer を通じて明示的に改行文字 (\n) を出力します。

これらの変更により、go list は、テンプレートが実際に何かを出力した場合にのみ改行を付加するようになり、出力のクリーンさが大幅に向上しました。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント (go list, text/template, bufio, io パッケージ)
  • Go言語のソースコード (src/cmd/go/list.go)
  • Go言語のコミット履歴
  • go list の使用例に関する一般的な情報源