[インデックス 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言語および関連技術の概念を理解しておく必要があります。
-
go list
コマンド:go list
はGo言語のツールチェーンの一部であり、Goパッケージに関する情報を表示するために使用されます。- 基本機能: 指定されたパッケージのパス、モジュール情報、依存関係などを表示します。
-f
フラグ: このフラグを使用すると、Goのtext/template
パッケージの構文を使って出力フォーマットをカスタマイズできます。例えば、go list -f '{{.Deps}}' mypackage
はmypackage
の直接の依存関係をリストします。.Name
と.Deps
: テンプレート内でアクセスできるパッケージ構造体 (Package
型) のフィールドです。.Name
はパッケージ名、.Deps
はそのパッケージが直接依存しているパッケージのリストを表します。println
関数: テンプレート内で使用できる関数で、引数を標準出力に書き込み、その後に改行を追加します。
-
io.Writer
インターフェース: Go言語における基本的なI/Oインターフェースの一つです。データを書き込むための抽象化を提供します。Write(p []byte) (n int, err error)
メソッドを持ち、バイトスライスp
を書き込み、書き込んだバイト数n
とエラーerr
を返します。os.Stdout
はio.Writer
インターフェースを実装しており、標準出力への書き込みに使用されます。
-
bufio.Writer
:io.Writer
をラップし、バッファリング機能を追加する型です。- 書き込み操作をバッファリングすることで、I/Oの効率を向上させます。
Flush()
メソッドを呼び出すことで、バッファに溜まったデータを強制的に基になるio.Writer
に書き出します。
-
Goの
text/template
パッケージ: Go言語に組み込まれているテキストテンプレートエンジンです。- データ構造をテンプレートに渡して、その構造体のフィールドやメソッドにアクセスし、動的にテキストを生成できます。
Execute(wr io.Writer, data interface{}) error
メソッドは、指定されたio.Writer
にテンプレートの実行結果を書き込みます。
-
改行の扱い: 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
}
このアプローチの問題点は以下の通りです。
println
関数による改行:go list -f '{{range .Deps}}{{println $.Name .}}{{end}}'
のようにテンプレート内でprintln
関数を使用すると、println
自体が改行を出力します。CountingWriter
はprintln
が出力した改行を単なるバイトとしてカウントするだけで、それが「改行」であるかどうかを区別しませんでした。- 余分な改行の発生: 各パッケージの処理の最後に
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
を返します。つまり、直前の出力が改行で終わっていない場合にのみ、追加の改行が必要であると判断します。- 初期値の工夫:
newTrackingWriter
でt.last
を\n
で初期化することで、最初のパッケージの出力の前には改行が不要であると正しく判断されます。 Reset()
の廃止:CountingWriter
のように各パッケージ処理前にカウントをリセットする必要がなくなりました。TrackingWriter
は常に直前の書き込みの状態のみを考慮するため、連続する出力に対して適切に動作します。
この変更により、println
関数がテンプレート内で改行を出力した場合でも、TrackingWriter
はそれを認識し、NeedNL()
が false
を返すため、余分な改行が追加されることがなくなりました。結果として、go list -f
の出力は、ユーザーが意図した通りの、余分な改行を含まないクリーンなものになります。
コアとなるコードの変更箇所
このコミットにおける主要なコード変更は、src/cmd/go/list.go
ファイルに集中しています。
-
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)
-
出力後の改行追加ロジックの変更: テンプレート実行後の改行追加の条件が
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'}) } } }
-
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.Writer
のFlush
メソッドを呼び出します。これにより、バッファに溜まっているデータが基になる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
コマンドは、テンプレートからの出力が既に改行を含んでいるかどうかを正確に判断し、必要に応じてのみ追加の改行を挿入するようになりました。これにより、出力の冗長性が排除され、よりクリーンで予測可能な結果が得られるようになりました。
関連リンク
- Go CL 7130052: https://golang.org/cl/7130052
参考にした情報源リンク
- Go Command
list
: https://pkg.go.dev/cmd/go#hdr-List_packages_or_modules - Go
text/template
package: https://pkg.go.dev/text/template - Go
io.Writer
interface: https://pkg.go.dev/io#Writer - Go
bufio.Writer
: https://pkg.go.dev/bufio#Writer - Go
os.Stdout
: https://pkg.go.dev/os#Stdout - Go
println
template function: (Goのテンプレート関数としてのprintln
は、text/template
パッケージのドキュメント内で直接言及されていますが、特定の独立したリンクはありません。一般的なGoの組み込み関数としてのprintln
とは異なります。) - Go言語のソースコード (src/cmd/go/list.go): https://github.com/golang/go/blob/master/src/cmd/go/list.go