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

[インデックス 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)のように明示的な型変換が必要になります。

panicrecoverによるエラーハンドリング

Go言語では、通常のエラーはerrorインターフェースを介して返されますが、プログラムの回復不可能な状態や予期せぬエラーに対してはpanicrecoverメカニズムが使用されます。

  • panic: 現在の関数の実行を停止し、呼び出し元の関数にパニックを伝播させます。最終的にプログラムをクラッシュさせます。
  • recover: deferされた関数内で呼び出されると、パニックを捕捉し、プログラムのクラッシュを防ぎ、パニックが発生した時点からの実行を再開させることができます。

このコミットでは、osErrorというカスタムエラー型をpanicrecoverのメカニズムと組み合わせて使用していましたが、それをより具体的なprinterErrorに置き換えることで、エラーの発生源を明確にしています。

技術的詳細

このコミットの主要な技術的変更点は、go/printerパッケージ内で使用されていた定数バイトスライスを削除し、代わりに直接文字列リテラルを使用するように変更したことです。これにより、コードの冗長性が減り、よりGoらしい記述になっています。

具体的な変更は以下の通りです。

  1. 定数バイトスライスの削除: 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
    -)
    
  2. osErrorからprinterErrorへの変更: エラーハンドリングに使用されていたカスタム型osErrorprinterErrorにリネームされました。これは、このエラーがオペレーティングシステム関連のエラーではなく、プリンターパッケージ内部で発生するエラーであることをより明確にするための変更です。

    -type osError struct {
    +type printerError struct {
    	err error
    }
    
  3. write0関数の引数変更と内部変換: write0関数は、p.outputio.Writerインターフェース)にデータを書き込むための内部ヘルパー関数です。この関数の引数が[]byteからstringに変更されました。しかし、io.WriterWriteメソッドは[]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を使用して最適化する意図があることを示しています。

  4. 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"
     				// ...
    
  5. その他の箇所での文字列リテラルの直接使用: 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))
    
  6. trimmer構造体におけるaNewlineの導入: trimmer構造体のWriteメソッドでは、newlines[0:1]の代わりに、新しく導入されたaNewlineという[]byte("\n")が使用されるようになりました。これは、trimmerio.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の扱い、およびエラー型の変更です。

  1. 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関数でformfeedsnewlinesの代わりに文字列リテラルを直接使用。
    • L240-L243: printer.writeItem関数でfmt.Sprintfの結果を直接stringとしてwrite0writeに渡すように変更。
    • 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関数でosErrorprinterErrorに置き換え。

コアとなるコードの解説

printer.goにおける変更の意図

このコミットの核心は、go/printerパッケージ内の文字列処理をよりGoのイディオムに近づけ、コードの明瞭性を高めることにあります。

  1. 定数バイトスライスの削除と文字列リテラルの直接使用: 以前は、\n\tのような単一文字や短い文字列であっても、[]byte{'\n'}[]byte("\t\t...")のように[]byteスライスとしてグローバルに定義されていました。これは、io.Writerインターフェースが[]byteを受け取るため、あるいは初期のGoの最適化戦略として行われていた可能性があります。 しかし、このコミットではこれらの冗長な定義を削除し、必要な箇所で直接"\n""\t"といった文字列リテラルを使用するように変更しました。これにより、コードはより簡潔になり、何が書き込まれているのかが一目でわかるようになりました。 例えば、p.write([]byte{' '})p.write(" ")になることで、コードの意図がより明確になります。

  2. write0およびwrite関数の引数変更: write0writeは、printerパッケージ内で実際にデータをp.outputに書き込むための主要なヘルパー関数です。これらの引数を[]byteからstringに変更したことは、printerパッケージの内部ロジックが文字列ベースで考えるようになったことを示しています。 ただし、write0関数内でp.output.Write([]byte(data))という変換が残っているのは、io.Writerインターフェースの制約によるものです。このTODOコメントは、この変換がパフォーマンス上のボトルネックになる可能性を認識しており、将来的にbytes.Bufferのようなメカニズムを使って、より効率的な書き込みを行うことを検討していることを示唆しています。bytes.Bufferを使用すれば、文字列を直接バッファに書き込み、最終的に一度だけ[]byteに変換してio.Writerに渡すことで、複数回の[]byte変換コストを削減できる可能性があります。

  3. osErrorからprinterErrorへの変更: panicrecoverメカニズムは、Go言語において予期せぬエラーや回復不可能な状態を扱うために使用されます。このコミットでは、printerパッケージ内で発生するエラーを示すために、汎用的なosError(オペレーティングシステムエラーを連想させる)から、より具体的なprinterErrorに型名を変更しました。これにより、recoverされたエラーがprinterパッケージ固有のものであることが明確になり、エラーハンドリングのロジックがより堅牢になります。これは、コードのセマンティクスを改善し、将来的なデバッグやメンテナンスを容易にするための良いプラクティスです。

  4. trimmerにおけるaNewlineの導入: trimmerは、出力の末尾の空白をトリムする役割を持つio.Writerの実装です。このWriteメソッド内では、io.Writerのインターフェース要件により[]byteを扱う必要があります。そのため、newlines[0:1](グローバルなnewlinesスライスの一部)の代わりに、[]byte("\n")という単一の改行バイトスライスをaNewlineという変数として導入しました。これは、グローバルなnewlinesスライスが削除されたことによる代替措置であり、trimmerの機能が正しく動作し続けることを保証するための変更です。

これらの変更は、個々のパフォーマンス最適化というよりも、コードベース全体の整合性、可読性、そしてGo言語のイディオムへの準拠を重視したリファクタリングと見ることができます。わずかなパフォーマンス低下は、コードの品質向上というメリットと、他の最適化によって相殺されるという見込みによって許容されました。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメントおよびブログ
  • Go言語のソースコード(src/pkg/go/printer/printer.go
  • Go言語のstring[]byteに関する一般的な知識
  • Go言語のエラーハンドリングに関する一般的な知識
  • Go言語のioパッケージに関する一般的な知識