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

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

このコミットは、Go言語の標準ライブラリgo/scannerパッケージにおいて、スキャンモードを表す型を汎用的なuintから、より明示的なscanner.Mode型へと変更するリファクタリングを行っています。これにより、コードの可読性、型安全性、および保守性が向上しています。go/parserパッケージ内の関連する箇所も、この新しい型定義に合わせて更新されています。

コミット

  • コミットハッシュ: 8b60613b921364a3674f3ef4d7a373e87a9fa280
  • 作者: Robert Griesemer gri@golang.org
  • コミット日時: 2012年1月24日 火曜日 16:49:03 -0800

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

https://github.com/golang/go/commit/8b60613b921364a3674f3ef4d7a373e87a9fa280

元コミット内容

go/scanner: Use explicit scanner.Mode type.

R=r, bradfitz
CC=golang-dev
https://golang.org/cl/5574059

変更の背景

この変更の主な背景は、Go言語のgo/scannerパッケージにおけるスキャンモードの表現を、よりセマンティックで型安全なものにすることです。以前は、スキャンモードは単なるuint(符号なし整数)として扱われていました。uintは非常に汎用的な型であり、その値が何を表しているのか、どのようなフラグが設定されているのかがコードを読むだけでは直感的に理解しにくいという問題がありました。

特定の目的のためにカスタム型を定義することで、以下の利点が得られます。

  1. 可読性の向上: scanner.Modeという型名自体が、その変数がスキャンモードに関するものであることを明確に示します。
  2. 型安全性: scanner.Mode型として定義することで、誤って他のuint型の値(例えば、ファイルサイズやカウントなど)をスキャンモードとして渡してしまうようなバグを防ぐことができます。コンパイラが型ミスマッチを検出し、開発段階でエラーを特定できるようになります。
  3. 自己文書化: コード自体がその意図をより明確に表現するようになります。
  4. 将来的な拡張性: Mode型にメソッドを追加したり、関連する定数をグループ化したりする際に、より構造化された方法で管理できるようになります。

このコミットは、Go言語の標準ライブラリが、より堅牢で保守性の高いコードベースを目指すという一般的な方針に沿ったものです。

前提知識の解説

このコミットを理解するためには、以下のGo言語の概念とパッケージに関する知識が必要です。

  1. go/scannerパッケージ:

    • Go言語のソースコードを字句解析(トークン化)するためのパッケージです。ソースコードの文字列を入力として受け取り、キーワード、識別子、演算子、リテラルなどの個々の「トークン」に分解します。
    • Scanner構造体は、この字句解析の主要なコンポーネントであり、ソースコードの読み込み、現在の文字の追跡、次のトークンの生成などのロジックを含んでいます。
    • Modeは、スキャナーの動作を制御するためのフラグの集合です。例えば、コメントをトークンとして返すかどうかなどを指定できます。
  2. go/parserパッケージ:

    • go/scannerによって生成されたトークンストリームを受け取り、Go言語の構文木(AST: Abstract Syntax Tree)を構築するためのパッケージです。
    • parser構造体は、構文解析の主要なコンポーネントです。
  3. tokenパッケージ:

    • Go言語のソースコードを構成するトークン(識別子、キーワード、演算子など)とその位置情報(ファイル、行、列)を定義するパッケージです。
    • token.FileSetは、複数のソースファイルにわたる位置情報を管理するための構造体です。
  4. 型定義 (typeキーワード):

    • Go言語では、既存の型(プリミティブ型や構造体など)に基づいて新しい型を定義できます。
    • type NewType UnderlyingType の形式で定義され、NewTypeUnderlyingTypeと同じ基底型を持ちますが、コンパイラにとっては異なる型として扱われます。これにより、型安全性が向上します。
  5. ビットフラグとiota:

    • ScanCommentsdontInsertSemisのような定数は、ビットフラグとして使用されます。これは、複数のブール値の状態を単一の整数値で表現する効率的な方法です。各フラグは、整数の特定のビットに対応します。
    • iotaは、Go言語のconst宣言内で使用される特別な識別子で、連続する整数値を自動的に生成します。1 << iotaの形式で使うと、1248...といった2のべき乗の値を生成し、ビットフラグの定義に非常に便利です。
    const (
        FlagA = 1 << iota // 1 (0001)
        FlagB             // 2 (0010)
        FlagC             // 4 (0100)
    )
    

    このコミットでは、Mode = 1 << iotaとすることで、ScanCommentsdontInsertSemisMode型を持つビットフラグとして定義されています。

技術的詳細

このコミットの技術的な核心は、Go言語の型システムを活用して、特定の目的を持つ整数値をより厳密に扱うようにした点にあります。

以前のコードでは、go/scannerパッケージのScanner構造体のmodeフィールドや、Init関数のmode引数、そしてgo/parserパッケージのparser構造体内のm変数などが、すべて汎用的なuint型として宣言されていました。

// 変更前 (src/pkg/go/scanner/scanner.go)
type Scanner struct {
    // ...
    mode uint // scanning mode
    // ...
}

func (S *Scanner) Init(file *token.File, src []byte, err ErrorHandler, mode uint) {
    // ...
}

// 変更前 (src/pkg/go/parser/parser.go)
func (p *parser) init(fset *token.FileSet, filename string, src []byte, mode Mode) {
    // ...
    var m uint // ここもuint
    // ...
}

このコミットでは、src/pkg/go/scanner/scanner.goに新しい型Modeが導入されました。

// 変更後 (src/pkg/go/scanner/scanner.go)
type Mode uint

このMode型はuintを基底型としていますが、Goコンパイラにとってはuintとは異なる型として扱われます。これにより、ScannermodeフィールドやInit関数の引数、そしてparser内の関連変数をこの新しいMode型に置き換えることで、コンパイラが型チェックをより厳密に行うようになります。

また、ScanCommentsdontInsertSemisといったスキャンモードの定数も、この新しいMode型を持つように変更されました。

// 変更後 (src/pkg/go/scanner/scanner.go)
const (
    ScanComments    Mode = 1 << iota // return comments as COMMENT tokens
    dontInsertSemis                  // do not automatically insert semicolons - for testing only
)

これにより、これらの定数がscanner.Mode型の値として扱われることが明確になり、uint型の変数に誤って代入されることを防ぎます。

この変更は、機能的な振る舞いを変更するものではなく、コードの内部的な健全性と保守性を向上させるための純粋なリファクタリングです。Go言語の設計思想である「明示性」と「シンプルさ」に沿った改善と言えます。

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

このコミットで変更された主要なファイルとコードの変更箇所は以下の通りです。

src/pkg/go/parser/parser.go

--- a/src/pkg/go/parser/parser.go
+++ b/src/pkg/go/parser/parser.go
@@ -54,7 +54,7 @@ type parser struct {

 func (p *parser) init(fset *token.FileSet, filename string, src []byte, mode Mode) {
 	p.file = fset.AddFile(filename, fset.Base(), len(src))
-	var m uint
+	var m scanner.Mode
 	if mode&ParseComments != 0 {
 		m = scanner.ScanComments
 	}
  • var m uintvar m scanner.Mode に変更されました。parserinit関数内で使用されるローカル変数mの型が、scannerパッケージで定義された新しいMode型に更新されています。

src/pkg/go/scanner/scanner.go

--- a/src/pkg/go/scanner/scanner.go
+++ b/src/pkg/go/scanner/scanner.go
@@ -40,7 +40,7 @@ type Scanner struct {
 	dir  string       // directory portion of file.Name()
 	src  []byte       // source
 	err  ErrorHandler // error reporting; or nil
-	mode uint         // scanning mode
+	mode Mode         // scanning mode

 	// scanning state
 	ch         rune // current character
@@ -86,12 +86,14 @@ func (S *Scanner) next() {
 	}
 }

-// The mode parameter to the Init function is a set of flags (or 0).
+// A mode value is set of flags (or 0).
 // They control scanner behavior.
 //
+type Mode uint
+
 const (
-\tScanComments    = 1 << iota // return comments as COMMENT tokens
-\tdontInsertSemis             // do not automatically insert semicolons - for testing only
+\tScanComments    Mode = 1 << iota // return comments as COMMENT tokens
+\tdontInsertSemis                  // do not automatically insert semicolons - for testing only
 )

 // Init prepares the scanner S to tokenize the text src by setting the
@@ -109,7 +111,7 @@ const (
 // Note that Init may call err if there is an error in the first character
 // of the file.
 //
-func (S *Scanner) Init(file *token.File, src []byte, err ErrorHandler, mode uint) {
+func (S *Scanner) Init(file *token.File, src []byte, err ErrorHandler, mode Mode) {
 	// Explicitly initialize all fields since a scanner may be reused.
 	if file.Size() != len(src) {
 		panic("file size does not match src len")
  • Scanner構造体のmodeフィールドの型が uint から Mode に変更されました。
  • type Mode uint という新しい型定義が追加されました。
  • ScanCommentsdontInsertSemis の定数定義が Mode = 1 << iota の形式に変更され、明示的にMode型を持つようになりました。
  • Scanner.Init関数のmode引数の型が uint から Mode に変更されました。

src/pkg/go/scanner/scanner_test.go

--- a/src/pkg/go/scanner/scanner_test.go
+++ b/src/pkg/go/scanner/scanner_test.go
@@ -281,7 +281,7 @@ func TestScan(t *testing.T) {
 	}
 }

-func checkSemi(t *testing.T, line string, mode uint) {
+func checkSemi(t *testing.T, line string, mode Mode) {
 	var S Scanner
 	file := fset.AddFile("TestSemis", fset.Base(), len(line))
 	S.Init(file, []byte(line), nil, mode)
  • checkSemiテスト関数のmode引数の型が uint から Mode に変更されました。これは、scanner.Init関数のシグネチャ変更に合わせたものです。

コアとなるコードの解説

このコミットのコアとなる変更は、src/pkg/go/scanner/scanner.goにおけるtype Mode uintの導入と、それに伴う関連箇所の型変更です。

  1. type Mode uintの導入:

    • これは、Go言語の型エイリアス(または基底型を持つ新しい型定義)の典型的な使用例です。Modeは内部的にはuintと同じビット表現を持ちますが、コンパイラはModeuintを異なる型として扱います。
    • これにより、Scannerの動作モードを表す値が、単なる汎用的な整数ではなく、「スキャナーのモード」という特定の意味を持つことがコード上で明確になります。
  2. Scanner構造体フィールドの型変更:

    • type Scanner struct { ... mode uint ... } から type Scanner struct { ... mode Mode ... } への変更は、Scannerインスタンスが保持するモード情報が、定義されたMode型に厳密に従うことを保証します。これにより、Scannerの内部状態の整合性が向上します。
  3. 定数定義の型付け:

    • const ( ScanComments = 1 << iota ... ) から const ( ScanComments Mode = 1 << iota ... ) への変更は重要です。これにより、ScanCommentsdontInsertSemisといった定数が、コンパイル時にMode型として扱われるようになります。
    • 以前は、これらの定数は型なしの整数定数として扱われ、uint型の変数に代入される際に暗黙的に型付けされていました。明示的にMode型を割り当てることで、これらの定数がscanner.Modeの文脈でのみ使用されるべきであることが強調されます。
  4. 関数シグネチャの変更:

    • func (S *Scanner) Init(..., mode uint) から func (S *Scanner) Init(..., mode Mode) への変更は、Scannerの初期化時に渡されるモード引数が、必ずMode型であることを要求します。これにより、Init関数の呼び出し元が、意図しない型の値を渡すことを防ぎ、APIの利用方法を明確にします。
    • 同様に、go/parserinit関数やscanner_test.gocheckSemi関数におけるmode引数の型もscanner.Modeに変更されており、パッケージ間の整合性が保たれています。

これらの変更は、Go言語の型システムをより効果的に利用し、コードの意図を明確にし、潜在的なバグをコンパイル時に検出できるようにするためのベストプラクティスを反映しています。

関連リンク

参考にした情報源リンク

  • 上記のGo言語公式ドキュメント
  • Go言語における型定義と型エイリアスに関する一般的な情報源(Go言語の書籍やチュートリアルなど)
  • ビットフラグとiotaのGo言語での使用例に関する一般的な情報源