[インデックス 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/templatepackage: https://pkg.go.dev/text/template - Go
io.Writerinterface: 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
printlntemplate 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