[インデックス 10465] ファイルの概要
このコミットは、Go言語の標準ライブラリgo/printer
パッケージにおける文字列とバイトスライス間の変換処理を整理し、コードの可読性と簡潔性を向上させることを目的としています。具体的には、定数文字列を[]byte
として事前に定義するのではなく、直接string
として渡し、必要に応じて内部で[]byte
に変換するように変更されています。これにより、コードがより自然なGoのイディオムに沿った形になり、冗長な定義が削減されています。
コミット
commit 82182514989c9872b9bc3be35c4fb02cf8d82a5b
Author: Robert Griesemer <gri@golang.org>
Date: Fri Nov 18 20:55:35 2011 -0800
go/printer: cleanup more string/byte conversions
Slight slow-down for printer benchmark (-0.7%) before
applying CL 5416049 (which will wash it out). Code is
cleaner and simpler.
R=rsc
CC=golang-dev
https://golang.org/cl/5417053
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/82182514989c9872b9bc3be35c4fb02cf8d82a5b
元コミット内容
Go言語のgo/printer
パッケージにおいて、文字列とバイトスライス間の変換処理をさらに整理しました。この変更により、プリンターのベンチマークでわずかな速度低下(-0.7%)が見られましたが、これは後続の変更(CL 5416049)によって相殺される予定です。コードはよりクリーンでシンプルになりました。
変更の背景
Go言語では、文字列(string
)とバイトスライス([]byte
)は異なる型であり、それぞれ不変なバイト列と可変なバイト列を表します。初期のGo言語のコードベースでは、パフォーマンス上の理由や特定のAPIの要件から、定数文字列であっても[]byte
として事前に定義し、それを使用するパターンが見られました。しかし、これはコードの可読性を損ね、冗長性を生む可能性がありました。
このコミットの背景には、go/printer
パッケージのコードベースをよりGoらしいイディオムに沿った形に整理し、簡潔性を高めるという意図があります。特に、頻繁に利用される定数文字列(例: "\n"
, "\t"
)を[]byte
として保持するのではなく、直接string
として扱うことで、コードがより直感的になります。
コミットメッセージにあるように、この変更はプリンターのベンチマークでわずかな速度低下を引き起こしましたが、これは別の変更(CL 5416049)によって相殺されることが見込まれていました。これは、コードのクリーンさとシンプルさを優先し、パフォーマンスへの影響が許容範囲内である、あるいは他の最適化でカバーされるという判断があったことを示唆しています。
前提知識の解説
Go言語におけるstring
と[]byte
string
: Go言語のstring
型は、不変なバイト列を表します。UTF-8エンコードされたテキストを扱うのに適しており、文字列リテラルはデフォルトでstring
型です。string
は内部的には読み取り専用のバイトスライスと長さを保持しています。[]byte
: バイトスライスは、可変なバイト列を表します。ファイルI/Oやネットワーク通信など、バイナリデータを扱う際によく使用されます。string
から[]byte
への変換、またはその逆の変換は、新しいメモリ割り当てとデータのコピーを伴うため、パフォーマンスに影響を与える可能性があります。
go/printer
パッケージ
go/printer
パッケージは、Go言語の抽象構文木(AST)を整形してGoのソースコードとして出力するためのパッケージです。コードのフォーマット、インデント、コメントの扱いなどを制御し、go fmt
コマンドの基盤の一部を形成しています。このパッケージは、コードの構造を正確に表現しつつ、読みやすい出力を生成するために、空白文字や改行、タブなどの細かい制御を頻繁に行います。
io.Writer
インターフェース
Go言語のio.Writer
インターフェースは、データを書き込むための汎用的なインターフェースです。その定義は以下の通りです。
type Writer interface {
Write(p []byte) (n int, err error)
}
このインターフェースは[]byte
型の引数を受け取るため、string
型のデータをio.Writer
に書き込む際には、[]byte(myString)
のように明示的な型変換が必要になります。
panic
とrecover
によるエラーハンドリング
Go言語では、通常のエラーはerror
インターフェースを介して返されますが、プログラムの回復不可能な状態や予期せぬエラーに対してはpanic
とrecover
メカニズムが使用されます。
panic
: 現在の関数の実行を停止し、呼び出し元の関数にパニックを伝播させます。最終的にプログラムをクラッシュさせます。recover
:defer
された関数内で呼び出されると、パニックを捕捉し、プログラムのクラッシュを防ぎ、パニックが発生した時点からの実行を再開させることができます。
このコミットでは、osError
というカスタムエラー型をpanic
とrecover
のメカニズムと組み合わせて使用していましたが、それをより具体的なprinterError
に置き換えることで、エラーの発生源を明確にしています。
技術的詳細
このコミットの主要な技術的変更点は、go/printer
パッケージ内で使用されていた定数バイトスライスを削除し、代わりに直接文字列リテラルを使用するように変更したことです。これにより、コードの冗長性が減り、よりGoらしい記述になっています。
具体的な変更は以下の通りです。
-
定数バイトスライスの削除:
esc
,htab
,htabs
,newlines
,formfeeds
といった、頻繁に使用される定数バイトスライスがグローバル変数から削除されました。これらは、それぞれエスケープ文字、タブ、複数のタブ、複数の改行、複数のフォームフィードを表していました。-var ( - esc = []byte{tabwriter.Escape} - htab = []byte{'\t'} - htabs = []byte("\t\t\t\t\t\t\t\t") - newlines = []byte("\n\n\n\n\n\n\n\n") // more than the max determined by nlines - formfeeds = []byte("\f\f\f\f\f\f\f\f") // more than the max determined by nlines -)
-
osError
からprinterError
への変更: エラーハンドリングに使用されていたカスタム型osError
がprinterError
にリネームされました。これは、このエラーがオペレーティングシステム関連のエラーではなく、プリンターパッケージ内部で発生するエラーであることをより明確にするための変更です。-type osError struct { +type printerError struct { err error }
-
write0
関数の引数変更と内部変換:write0
関数は、p.output
(io.Writer
インターフェース)にデータを書き込むための内部ヘルパー関数です。この関数の引数が[]byte
からstring
に変更されました。しかし、io.Writer
のWrite
メソッドは[]byte
を受け取るため、関数内部で[]byte(data)
という明示的な変換が追加されました。-func (p *printer) write0(data []byte) { +func (p *printer) write0(data string) { if len(data) > 0 { - n, err := p.output.Write(data) + // TODO(gri) Replace bottleneck []byte conversion + // with writing into a bytes.Buffer. + // Will also simplify post-processing. + n, err := p.output.Write([]byte(data)) p.written += n if err != nil { - panic(osError{err}) + panic(printerError{err}) } } }
TODO
コメントは、この[]byte
変換がボトルネックになる可能性を認識しており、将来的にbytes.Buffer
を使用して最適化する意図があることを示しています。 -
write
関数の引数変更と文字列リテラルの直接使用:write
関数も同様に引数が[]byte
からstring
に変更されました。この関数内では、以前はグローバル変数として定義されていたhtabs
の代わりに、ローカル定数としてconst htabs = "\t\t\t\t\t\t\t\t"
が定義され、直接使用されるようになりました。-func (p *printer) write(data []byte) { +func (p *printer) write(data string) { i0 := 0 - for i, b := range data { - switch b { + for i := 0; i < len(data); i++ { + switch data[i] { case '\n', '\f': - // write segment ending in b + // write segment ending in data[i] p.write0(data[i0 : i+1]) // ... if p.mode&inLiteral == 0 { // write indentation + const htabs = "\t\t\t\t\t\t\t\t" // ...
-
その他の箇所での文字列リテラルの直接使用:
writeNewlines
,writeItem
,writeCommentPrefix
,writeComment
,writeCommentSuffix
,intersperseComments
,writeWhitespace
など、[]byte
定数を使用していた多くの箇所で、対応する文字列リテラル(例:"\f\f\f\f"
,"\n\n\n\n"
," "
,"\t"
,"\f"
,"\n"
)が直接使用されるようになりました。例:
- p.write(formfeeds[0:n]) + p.write("\f\f\f\f"[0:n]) // ... - p.write(newlines[0:n]) + p.write("\n\n\n\n"[0:n]) // ... - p.write([]byte(fmt.Sprintf("..."))) + p.write0(fmt.Sprintf("...")) // ... - p.write([]byte(data)) + p.write(data) // ... - p.write([]byte{' '}) + p.write(" ") // ... - p.write(htab) + p.write("\t") // ... - p.write(linebreak) // linebreak was formfeeds[0:1] + p.write("\f") // ... - p.write([]byte{'\n'}) + p.write("\n") // ... - p.write([]byte{' '}) + p.write(" ") // ... - data[0] = byte(ch) - p.write(data[0:]) + p.write(string(ch))
-
trimmer
構造体におけるaNewline
の導入:trimmer
構造体のWrite
メソッドでは、newlines[0:1]
の代わりに、新しく導入されたaNewline
という[]byte("\n")
が使用されるようになりました。これは、trimmer
がio.Writer
インターフェースを実装しており、Write
メソッドが[]byte
を受け取るため、特定のバイトスライスが必要だったためと考えられます。+var aNewline = []byte("\n") // ... - _, err = p.output.Write(newlines[0:1]) // write newline + _, err = p.output.Write(aNewline) // ... - _, err = p.output.Write(newlines[0:1]) // write newline + _, err = p.output.Write(aNewline)
これらの変更は、コードの意図をより明確にし、Goの型システムをより効果的に活用することを目的としています。string
リテラルを直接使用することで、コンパイラがより多くの最適化を行う機会も生まれる可能性があります。
コアとなるコードの変更箇所
このコミットにおけるコアとなる変更箇所は、主に以下の関数におけるstring
と[]byte
の扱い、およびエラー型の変更です。
src/pkg/go/printer/printer.go
- L33-L40: グローバルな
[]byte
定数群の削除。 - L58:
osError
型からprinterError
型へのリネーム。 - L143-L149:
printer.write0
関数の引数を[]byte
からstring
に変更し、内部で[]byte
への変換を追加。 - L157-L172:
printer.write
関数の引数を[]byte
からstring
に変更し、ループ処理とhtabs
定数の定義を変更。 - L211-L214:
printer.writeNewlines
関数でformfeeds
とnewlines
の代わりに文字列リテラルを直接使用。 - L240-L243:
printer.writeItem
関数でfmt.Sprintf
の結果を直接string
としてwrite0
とwrite
に渡すように変更。 - L301-L306:
printer.writeCommentPrefix
関数で空白とタブを文字列リテラルで直接指定。 - L573-L574:
printer.writeComment
関数でフォームフィードを文字列リテラルで直接指定。 - L617-L618:
printer.writeCommentSuffix
関数で改行を文字列リテラルで直接指定。 - L643-L646:
printer.intersperseComments
関数で空白を文字列リテラルで直接指定。 - L695-L698:
printer.writeWhitespace
関数でバイト変換をstring(ch)
に置き換え。 - L871:
aNewline
変数の導入。 - L889-L890, L917-L918:
trimmer.Write
メソッドでnewlines[0:1]
の代わりにaNewline
を使用。 - L992-L993, L1020-L1021:
fprint
関数でosError
をprinterError
に置き換え。
- L33-L40: グローバルな
コアとなるコードの解説
printer.go
における変更の意図
このコミットの核心は、go/printer
パッケージ内の文字列処理をよりGoのイディオムに近づけ、コードの明瞭性を高めることにあります。
-
定数バイトスライスの削除と文字列リテラルの直接使用: 以前は、
\n
や\t
のような単一文字や短い文字列であっても、[]byte{'\n'}
や[]byte("\t\t...")
のように[]byte
スライスとしてグローバルに定義されていました。これは、io.Writer
インターフェースが[]byte
を受け取るため、あるいは初期のGoの最適化戦略として行われていた可能性があります。 しかし、このコミットではこれらの冗長な定義を削除し、必要な箇所で直接"\n"
や"\t"
といった文字列リテラルを使用するように変更しました。これにより、コードはより簡潔になり、何が書き込まれているのかが一目でわかるようになりました。 例えば、p.write([]byte{' '})
がp.write(" ")
になることで、コードの意図がより明確になります。 -
write0
およびwrite
関数の引数変更:write0
とwrite
は、printer
パッケージ内で実際にデータをp.output
に書き込むための主要なヘルパー関数です。これらの引数を[]byte
からstring
に変更したことは、printer
パッケージの内部ロジックが文字列ベースで考えるようになったことを示しています。 ただし、write0
関数内でp.output.Write([]byte(data))
という変換が残っているのは、io.Writer
インターフェースの制約によるものです。このTODO
コメントは、この変換がパフォーマンス上のボトルネックになる可能性を認識しており、将来的にbytes.Buffer
のようなメカニズムを使って、より効率的な書き込みを行うことを検討していることを示唆しています。bytes.Buffer
を使用すれば、文字列を直接バッファに書き込み、最終的に一度だけ[]byte
に変換してio.Writer
に渡すことで、複数回の[]byte
変換コストを削減できる可能性があります。 -
osError
からprinterError
への変更:panic
とrecover
メカニズムは、Go言語において予期せぬエラーや回復不可能な状態を扱うために使用されます。このコミットでは、printer
パッケージ内で発生するエラーを示すために、汎用的なosError
(オペレーティングシステムエラーを連想させる)から、より具体的なprinterError
に型名を変更しました。これにより、recover
されたエラーがprinter
パッケージ固有のものであることが明確になり、エラーハンドリングのロジックがより堅牢になります。これは、コードのセマンティクスを改善し、将来的なデバッグやメンテナンスを容易にするための良いプラクティスです。 -
trimmer
におけるaNewline
の導入:trimmer
は、出力の末尾の空白をトリムする役割を持つio.Writer
の実装です。このWrite
メソッド内では、io.Writer
のインターフェース要件により[]byte
を扱う必要があります。そのため、newlines[0:1]
(グローバルなnewlines
スライスの一部)の代わりに、[]byte("\n")
という単一の改行バイトスライスをaNewline
という変数として導入しました。これは、グローバルなnewlines
スライスが削除されたことによる代替措置であり、trimmer
の機能が正しく動作し続けることを保証するための変更です。
これらの変更は、個々のパフォーマンス最適化というよりも、コードベース全体の整合性、可読性、そしてGo言語のイディオムへの準拠を重視したリファクタリングと見ることができます。わずかなパフォーマンス低下は、コードの品質向上というメリットと、他の最適化によって相殺されるという見込みによって許容されました。
関連リンク
- Go言語の文字列とバイトスライス: https://go.dev/blog/strings
go/printer
パッケージのドキュメント: https://pkg.go.dev/go/printerio.Writer
インターフェースのドキュメント: https://pkg.go.dev/io#Writer- Go言語における
panic
とrecover
: https://go.dev/blog/defer-panic-and-recover
参考にした情報源リンク
- Go言語の公式ドキュメントおよびブログ
- Go言語のソースコード(
src/pkg/go/printer/printer.go
) - Go言語の
string
と[]byte
に関する一般的な知識 - Go言語のエラーハンドリングに関する一般的な知識
- Go言語の
io
パッケージに関する一般的な知識